Function Calls
Building upon our understanding of function definition and methods, let's now analyze the process triggered when a function is called. In the following, all our explanations will be based on the following function foo
:
In Julia, defining a function like foo(a, b)
is shorthand for creating a method with the signature foo(a::Any, b::Any)
. Thus, the function body foo(a,b)
holds for all possible type combinations of a
and b
.
When foo(1, 2)
is called, Julia evaluates the expression 2 + a * b
by following a series of steps.
The process begins with what's known as multiple dispatch, where Julia selects which method of a function to execute. Importantly, this decision is based solely on the types of the arguments, not their values. Specifically, Julia begins by identifying the concrete types of the function arguments. In our example where a = 1
and b = 2
, both are identified as Int64
. The information on types is then used to select a method, which defines the function body and hence the operations to be performed. This process involves searching through the available methods of foo
to find the most specific one for the concrete types of a
and b
. In our example, foo
has only one method foo(a,b) = 2 + a * b
, which is defined for all argument types, including a::Int64
and b::Int64
. Therefore, the corresponding function body is 2 + a * b
.
The specific version of this method for the signature foo(a::Int64, b::Int64)
is known as a method instance. If the code for the method instance foo(a::Int64, b::Int64)
already exists, Julia will directly employ it to compute foo(1,2)
. Otherwise, the compiler generates optimized code for that method instance, stores it in memory for future use, and Julia executes it.
The following diagram depicts the process unfolded when foo(1,2)
is executed.
Multiple Dispatch
The process outlined has implications for how the language works. When a function is called with a particular combination of argument types for the first time, Julia incurs an additional cost because it must generate specialized code for those types. This initial delay is often referred to as Time To First Plot, a phrase that highlights how the compilation overhead becomes noticeable in interactive workflows such as plotting. Once the code has been compiled, however, Julia stores the resulting method instance, so that subsequent calls with the same argument types can reuse it. This eliminates the compilation step and leads to much faster execution.
The example with foo
illustrates this process clearly. After evaluating foo(1, 2)
, Julia has already compiled a method instance for the signature foo(a::Int64, b::Int64)
. This is why the subsequent call foo(3, 2)
can be executed immediately by invoking the cached method instance, without any need for recompilation. Instead, the execution of foo(3.0, 2)
introduces a new combination of argument types, where a::Float64
and b::Int64
. Because no compiled method instance yet exists for this signature, Julia must generate one before executing the function.