Function Call
Given a function and its methods, we can now analyze the process triggered when a function is called. In the following, we'll base our explanations on the following function foo
:
Recall that variables with no type annotation default to Any
. This implies that the function body foo(a,b)
holds for any combination of types of a
and b
.
Multiple Dispatch
When foo(1, 2)
is called, Julia is instructed to evaluate the expression 2 + a * b
. This process relies on a mechanism known as multiple dispatch, where Julia decides on the computational approach to be implemented. Importantly, this decision is based on solely on the types of the arguments, not their values.
Multiple dispatch proceeds in several steps. First, the compiler determines the concrete types of the function arguments. In our example, since a = 1
, and b = 2
, both are identified as Int64
.
After this, the information on types is used to select a method, which defines the function body and hence the operations to be performed. This process involves searching through all available methods of foo
until a method that matches the concrete types of a
and b
is identified. In our example, foo
has only one method foo(a,b) = 2 + a * b
, which applies to all type combinations of a
and b
. Consequently, the relevant function body is 2 + a * b
.
The operations to be performed are then forwarded to the compiler, which is in charge of the implementation. This involves choosing a method instance, which refers to the specific code implementation that will be used to compute the operations defined by the method.
If a method instance already exists for the function signature foo(a::Int64, b::Int64)
, Julia will directly employ it to compute foo(1,2)
. Otherwise, a new method instance is generated and stored (cached) in memory.
The following diagram depicts all the process unfolded when foo(1,2)
is executed.
Multiple Dispatch
The process determines that the first time you call a function with particular argument types, there’s an initial compilation overhead. This phenomenon is referred to in Julia as Time To First Plot. Instead, subsequent calls with the same argument types can reuse this cached code, resulting in faster execution.
To illustrate this mechanism, note that the call foo(3, 2)
incorporated in the example occurs after foo(1,2)
. This allows Julia to compute foo(3, 2)
by directly invoking the method instance foo(a::Int64, b::Int64)
, without the need of compiling code. Instead, executing a function call like foo(3.0, 2)
requires the compilation of a new method instance foo(a::Float64, b::Int64)
, since the types of 3
and 3.0
differ.