I am trying to get a better conceptual understanding of how the Julia JIT compilation process (perhaps better phrased as "compilation sequence") works.
Specifically, I am interested to know how Julia handles function return values.
For context, I am coming from this from a background of C/C++/x86 ASM.
Allow me to describe, briefly, how compilation of functions works in this context. My explanation will not be particularly detailed, but I hope it will be sufficient to illustrate the point.
Let's consider the following C++ code.
void example_function() {
auto tmp = another_function();
std::cout << tmp << std::endl;
return;
}
std::vector<double> another_function() {
std::vector<double> tmp;
tmp.push_back(1.0);
return tmp;
}
Here is a list of the important concepts illustrated in this example.
std::vector<double>
cout
statement - just assume an implementation of std::cout
for a std::vector<double>
is provided somewhere. It's just there to illustrate the return value being used for somethingIn a static language such as C++, the return value types are known at compile time. Typically, the compiler will allocate some space on the stack for the return value before calling a function.
In this example, before calling another_function
, the compiler typically will push a block of stack memory onto the stack. The size of this block is large enough to hold the return value. This size is known at compile time, because the compiler knows (at compile time) the return type, and therefore the size of the return type.
Typically the compiler will do this "stack push" operation by adding (or subtracting?) from the stack pointer. (If I recall correctly?)
The point is, the return value size is known at compile time.
A Julia function can return multiple types. Here are some questions:
I am trying to get a better conceptual understanding of how the Julia JIT compilation process (perhaps better phrased as "compilation sequence") works.
Specifically, I am interested to know how Julia handles function return values.
For context, I am coming from this from a background of C/C++/x86 ASM.
Allow me to describe, briefly, how compilation of functions works in this context. My explanation will not be particularly detailed, but I hope it will be sufficient to illustrate the point.
Let's consider the following C++ code.
void example_function() {
auto tmp = another_function();
std::cout << tmp << std::endl;
return;
}
std::vector<double> another_function() {
std::vector<double> tmp;
tmp.push_back(1.0);
return tmp;
}
Here is a list of the important concepts illustrated in this example.
std::vector<double>
cout
statement - just assume an implementation of std::cout
for a std::vector<double>
is provided somewhere. It's just there to illustrate the return value being used for somethingIn a static language such as C++, the return value types are known at compile time. Typically, the compiler will allocate some space on the stack for the return value before calling a function.
In this example, before calling another_function
, the compiler typically will push a block of stack memory onto the stack. The size of this block is large enough to hold the return value. This size is known at compile time, because the compiler knows (at compile time) the return type, and therefore the size of the return type.
Typically the compiler will do this "stack push" operation by adding (or subtracting?) from the stack pointer. (If I recall correctly?)
The point is, the return value size is known at compile time.
A Julia function can return multiple types. Here are some questions:
To make a long story short, there are basically two flavors of Julia functions: those where the return type can be inferred, and those where it can't be. When it can be, then you basically get the equivalent of a C++ function. When it can't be, or it's known to be one of several types, then the return value gets boxed and you lose a lot of performance.
To give a slightly longer answer, the answer to this depends on the calling convention that the compiler chooses to use for the function. Julia's compiler will choose a calling convention based on what it knows about the function. If it doesn't know the input types that the function will be called with, it will "box" all the arguments and return value (basically putting them in a heap allocated object that stores the type). However, most of the time, the compiler is able to figure out what the code is doing (via type inference), and can use a specialized calling convention for the argument types.