Loops and variables¶
Consider this C function:
int loop_test (int n) { int sum = 0; for (int i = 0; i < n; i++) sum += i * i; return sum; }
This example demonstrates some more features of libgccjit, with local variables and a loop.
Let’s construct this from Python. To break this down into libgccjit terms, it’s usually easier to reword the for loop as a while loop, giving:
int loop_test (int n) { int sum = 0; int i = 0; while (i < n) { sum += i * i; i++; } return sum; }
Here’s what the final control flow graph will look like:
As before, we import the libgccjit Python bindings and make a
gccjit.Context
:
>>> import gccjit
>>> ctxt = gccjit.Context()
The function works with the C int type:
>>> the_type = ctxt.get_type(gccjit.TypeKind.INT)
though we could equally well make it work on, say, double:
>>> the_type = ctxt.get_type(gccjit.TypeKind.DOUBLE)
Let’s build the function:
>>> return_type = the_type
>>> param_n = ctxt.new_param(the_type, b"n")
>>> fn = ctxt.new_function(gccjit.FunctionKind.EXPORTED,
... return_type,
... b"loop_test",
... [param_n])
>>> print(fn)
loop_test
The base class of expression is the gccjit.RValue
,
representing an expression that can be on the right-hand side of
an assignment: a value that can be computed somehow, and assigned
to a storage area (such as a variable). It has a specific
gccjit.Type
.
Anothe important class is gccjit.LValue
.
A gccjit.LValue
is something that can of the left-hand
side of an assignment: a storage area (such as a variable).
In other words, every assignment can be thought of as:
LVALUE = RVALUE;
Note that gccjit.LValue
is a subclass of
gccjit.RValue
, where in an assignment of the form:
LVALUE_A = LVALUE_B;
the LVALUE_B implies reading the current value of that storage area, assigning it into the LVALUE_A.
So far the only expressions we’ve seen are i * i:
ctxt.new_binary_op(gccjit.BinaryOp.MULT,
int_type,
param_i, param_i)
which is a gccjit.RValue
, and the various function
parameters: param_i and param_n, instances of
gccjit.Param
, which is a subclass of
gccjit.LValue
(and, in turn, of gccjit.RValue
):
we can both read from and write to function parameters within the
body of a function.
Our new example has a couple of local variables. We create them by
calling gccjit.Function.new_local()
, supplying a type and a name:
>>> local_i = fn.new_local(the_type, b"i")
>>> print(local_i)
i
>>> local_sum = fn.new_local(the_type, b"sum")
>>> print(local_sum)
sum
These are instances of gccjit.LValue
- they can be read from
and written to.
Note that there is no precanned way to create and initialize a variable like in C:
int i = 0;
Instead, having added the local to the function, we have to separately add an assignment of 0 to local_i at the beginning of the function.
This function has a loop, so we need to build some basic blocks to handle the control flow. In this case, we need 4 blocks:
before the loop (initializing the locals)
the conditional at the top of the loop (comparing i < n)
the body of the loop
after the loop terminates (return sum)
so we create these as gccjit.Block
instances within the
gccjit.Function
:
>>> entry_block = fn.new_block(b'entry')
>>> cond_block = fn.new_block(b"cond")
>>> loop_block = fn.new_block(b"loop")
>>> after_loop_block = fn.new_block(b"after_loop")
We now populate each block with statements.
The entry block consists of initializations followed by a jump to the
conditional. We assign 0 to i and to sum, using
gccjit.Block.add_assignment()
to add
an assignment statement, and using gccjit.Context.zero()
to
get the constant value 0 for the relevant type for the right-hand side
of the assignment:
>>> entry_block.add_assignment(local_i, ctxt.zero(the_type))
>>> entry_block.add_assignment(local_sum, ctxt.zero(the_type))
We can then terminate the entry block by jumping to the conditional:
>>> entry_block.end_with_jump(cond_block)
The conditional block is equivalent to the line while (i < n) from our
C example. It contains a single statement: a conditional, which jumps to
one of two destination blocks depending on a boolean
gccjit.RValue
, in this case the comparison of i and n.
We build the comparison using gccjit.Context.new_comparison()
:
>>> guard = ctxt.new_comparison(gccjit.Comparison.LT, local_i, param_n)
>>> print(guard)
i < n
and can then use this to add cond_block’s sole statement, via
gccjit.Block.end_with_conditional()
:
>>> cond_block.end_with_conditional(guard,
... loop_block, # on true
... after_loop_block) # on false
Next, we populate the body of the loop.
The C statement sum += i * i; is an assignment operation, where an
lvalue is modified “in-place”. We use
gccjit.Block.add_assignment_op()
to handle these operations:
>>> loop_block.add_assignment_op(local_sum,
... gccjit.BinaryOp.PLUS,
... ctxt.new_binary_op(gccjit.BinaryOp.MULT,
... the_type,
... local_i, local_i))
The i++ can be thought of as i += 1, and can thus be handled in
a similar way. We use gccjit.Context.one()
to get the constant
value 1 (for the relevant type) for the right-hand side
of the assignment:
>>> loop_block.add_assignment_op(local_i,
... gccjit.BinaryOp.PLUS,
... ctxt.one(the_type))
The loop body completes by jumping back to the conditional:
>>> loop_block.end_with_jump(cond_block)
Finally, we populate the after_loop block, reached when the loop conditional is false. At the C level this is simply:
return sum;
so the block is just one statement:
>>> after_loop_block.end_with_return(local_sum)
Note
You can intermingle block creation with statement creation, but given that the terminator statements generally include references to other blocks, I find it’s clearer to create all the blocks, then all the statements.
We’ve finished populating the function. As before, we can now compile it to machine code:
>>> jit_result = ctxt.compile()
>>> void_ptr = jit_result.get_code(b'loop_test')
and use ctypes to turn it into a Python callable:
>>> import ctypes
>>> int_int_func_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)
>>> callable = int_int_func_type(void_ptr)
Now we can call it:
>>> callable(10)
285
Visualizing the control flow graph¶
You can see the control flow graph of a function using
gccjit.Function.dump_to_dot()
:
>>> fn.dump_to_dot('/tmp/sum-of-squares.dot')
giving a .dot file in GraphViz format.
You can convert this to an image using dot:
$ dot -Tpng /tmp/sum-of-squares.dot -o /tmp/sum-of-squares.png
or use a viewer (my preferred one is xdot.py; see https://github.com/jrfonseca/xdot.py; on Fedora you can install it with yum install python-xdot):
Full example¶
Here’s what the above looks like as a complete program:
import ctypes import gccjit def populate_ctxt(ctxt): the_type = ctxt.get_type(gccjit.TypeKind.INT) return_type = the_type param_n = ctxt.new_param(the_type, b"n") fn = ctxt.new_function(gccjit.FunctionKind.EXPORTED, return_type, b"loop_test", [param_n]) # Build locals local_i = fn.new_local(the_type, b"i") local_sum = fn.new_local(the_type, b"sum") assert str(local_i) == 'i' # Build blocks entry_block = fn.new_block(b'entry') cond_block = fn.new_block(b"cond") loop_block = fn.new_block(b"loop") after_loop_block = fn.new_block(b"after_loop") # entry_block: ######################################### # sum = 0 entry_block.add_assignment(local_sum, ctxt.zero(the_type)) # i = 0 entry_block.add_assignment(local_i, ctxt.zero(the_type)) entry_block.end_with_jump(cond_block) ### cond_block: ######################################## # while (i < n) cond_block.end_with_conditional(ctxt.new_comparison(gccjit.Comparison.LT, local_i, param_n), loop_block, after_loop_block) ### loop_block: ######################################## # sum += i * i loop_block.add_assignment_op(local_sum, gccjit.BinaryOp.PLUS, ctxt.new_binary_op(gccjit.BinaryOp.MULT, the_type, local_i, local_i)) # i++ loop_block.add_assignment_op(local_i, gccjit.BinaryOp.PLUS, ctxt.one(the_type)) # goto cond_block loop_block.end_with_jump(cond_block) ### after_loop_block: ################################## # return sum after_loop_block.end_with_return(local_sum) def create_fn(): # Create a compilation context: ctxt = gccjit.Context() if 0: ctxt.set_bool_option(gccjit.BoolOption.DUMP_INITIAL_TREE, True) ctxt.set_bool_option(gccjit.BoolOption.DUMP_INITIAL_GIMPLE, True) ctxt.set_bool_option(gccjit.BoolOption.DUMP_EVERYTHING, True) ctxt.set_bool_option(gccjit.BoolOption.KEEP_INTERMEDIATES, True) if 0: ctxt.set_int_option(gccjit.IntOption.OPTIMIZATION_LEVEL, 3) populate_ctxt(ctxt) jit_result = ctxt.compile() return jit_result def test_calling_fn(i): jit_result = create_fn() int_int_func_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int) code = int_int_func_type(jit_result.get_code(b"loop_test")) return code(i) if __name__ == '__main__': print(test_calling_fn(10))