Toying with GCC JIT – Part 1
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 interpreter
-like process, like an interpreter of a interpreted programming language. Interpretation is in general much slower than execution of compiled code. In order to speedup the interpretation, these systems usually track those parts of the program that are hot, meaning that they are run a lot of times. When a part of the code (usually at a granularity of a function) becomes hot, the implementation of the interpreted language schedules a compilation of that code. When the code has been compiled, the implementation of the language switches from interpretation to the execution of compiled code. This technique, that may bring noticeable speed improvements, has been used in several programming languages including Java, JavaScript and Python.
GCC JIT
Note: I will use GCC JIT
but the official name of this component of GCC is libgccjit
.
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.
Installation
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.
The flag -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_object
s. 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 a
and b
. We will create also a function, add
, with two parameters (a
and 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: a
and 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 int
.
Now we need to create the function add
. It has two parameters a
and b
that we created earlier. It will return int
as well.
The parameter 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
Conditional code
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.
A loop
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 n
.
We will also need a couple of local variables i
and 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.
The init
block will initialize i
and s
to zero and then jump to the loop_header
block.
The block loop_header
checks if i < n
. If the check succeeds it will jump to loop_body
otherwise it will jump to return
.
The block 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 loop_header
.
Finally the block return
just returns the value currently in s
.
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.