A just-in-time (JIT) compiler is a compiler that in contrast to the usual compilers is not run ahead-of-time, i.e. before running the actual program, but during the program itself.
JIT compilers run during the execution of the program. Since compilation is a heavy process, the benefits brought by the compilation must clearly outweigh the time spent in compiling code. The typical scenarios where JITs are used are those where a program features a
Note: I will use
GCC JIT but the official name of this component of GCC is
Recently GCC has earned the ability to act as a just-in-time compiler. The JIT is integrated in the GCC compilation workflow as a front end language (like C, C++ or Fortran). For this post I will be using GCC 5.2.
Given that the system compiler is a sensitive piece of software and that it is unlikely that GCC JIT is enabled in your Linux distribution, we will have to build a GCC with that support enabled.
First, create a directory where we will put everything, enter it and download GCC 5.2
The next step involves building GCC. First we need to get some software required by GCC itself.
Now, create a build directory sibling to
gcc-5.2.0 and make sure you enter it.
Now configure the compiler and build it. In this step we will specify where the compiler will be installed. Make sure that you are in
gcc-build! This step takes several minutes (about 15 minutes or so, depending on your machine) but you only have to do it once.
Now install it
Now check the installation
That's it. You can find much more information about installing here.
Build programs that use the GCC JIT
The GCC JIT is designed as a library that offers a C interface. This interface is in
libgccjit.h and will be located in
$INSTALLDIR/include. The library itself is in
$INSTALLDIR/lib. Following is a
Makefile that can be used when compiling and linking a program that uses GCC JIT.
-Wl,-rpath,... is necessary as we installed GCC in a non-standard directory, and we will need to find
libgccjit.so when executing the program.
Also make sure that the
PATH environment variable contains
$GCCDIR (defined above), otherwise during the execution of our program,
libgccjit.so will not be able to invoke the compiler.
Our first JIT function
As a starter, we will JIT a very simple function that justs computes the addition of two numbers. Before anything we have to be aware that like in a real compiler, lots of things can go wrong before we get any actual code, so we should take care of all possible errors. We will use this very simple routine.
</p> To work with GCC JIT we need a context. </p>
Our goal is creating a function that adds two numbers, like the following.
GCC JIT works with entities called
gcc_jit_objects. These objects are of different kinds but for this first example we will need an object of kind type that represents the C
int type. We will also need two objects of kind parameter (with
int type) that will represent the parameters
b. We will create also a function,
add, with two parameters (
b) with a block containing a single expression that computes
a + b.
Let's get started. First get an object representing
int. Fundamental types (closely following the fundamental ones of C) are obtained using gcc_jit_context_get_type.
You will see that we will have to pass all the time the context we got at the beginning. Now we can create two parameters:
b. Beside the context, to create a parameter we will need to pass a location, a type and a name. Since we are creating a function out of thin air, we do not have any location so we will leave it
NULL. Note that we use the
int type we got above.
Now we can compute
a + b. Calculations are called rvalues (borrowing the concept from the C language): it means something that has to be computed. An rvalue can be created from a parameter using
gcc_jit_param_as_rvalue. This is a very simple calculation that means give me the value that the parameter has now.
Once we have the two parameters we can add them. This will yield another rvalue of type
Now we need to create the function
add. It has two parameters
b that we created earlier. It will return
int as well.
GCC_JIT_FUNCTION_EXPORTED means that it will be available out of the JIT code, otherwise the function can only be called from the JIT code itself. We will get later this
add function to call it from our C code.
Functions are constructed by blocks, which represent a sequence of calculations called statements that may have some visible effect. In our case, just adding two numbers has no effects unless the result of the addition is kept somewhere (i.e. in a variable), or used for something else (i.e. in the expression of the return statement or as the condition expression of an if-then-else statement). All statements inside a block are always executed in order: a block is never partially executed. If the code we want to generate has conditional parts each conditional part goes into a different block. When we create a block, we can give it a symbolic name that can be used for debugging. A block must be ended by transferring the control, either to another block (i.e. if-then-else, switch/case, loops, etc.) or leaving the function (i.e. return statement).
Now we end the block by just returning the addition we computed.
Ok, this may have looked like a bit slow but we are done with creating code. Next step is compiling it! Compiling a context will give us a result. If the result is non-null it will represent all the functions (and global variables if any) that we have created. From the result we will be able to get the exported functions and execute them. But let's compile first.
Now we can get the exported function
add. We will get a generic pointer, so we will have to cast apropiately before calling it.
Finally we have to release the result of the compilation and the JIT context.
More interesting examples
Our first function required a bit of boilerplate to build it but now that we understand the basics, we can move on to something more sophisticated. Our next function will be like this one.
The only difference here is using a new parameter of type bool and then ending conditionally the first block (after evaluating op) to either go to the true (then) or false (else) block.
We tell GCC JIT to dump a graph-like representation of the function that we have created in Graphviz. It will look like this.
Now we can compile the code and see if it works.
Now we want to implement something like this.
We start as usual, defining the parameter
n of type
int and creating a function that returns
int and receives the parameter
We will also need a couple of local variables
s to keep respectively the counter of the loop and the partial sum up to the
i - 1 value. We also get their rvalue object that we will use later.
To implement this function we will need four blocks.
init block will initialize
s to zero and then jump to the
loop_header checks if
i < n. If the check succeeds it will jump to
loop_body otherwise it will jump to
loop_body computes both
s = s + i and
i = i + 1. These two operations can be compacted as their equivalents
s += i and
i += 1. After this it jumps back to
Finally the block
return just returns the value currently in
The Graphviz representation of this function looks like this.
As before, we can compile and test this function.
You may want to check libgccjit documentation for more details, examples and a tutorial. In the next part of this post, we will apply GCC JIT to a very simple regular expression matcher code.
That's all for today.