Creating a trivial machine code function — gccjit 0.4 documentation (original) (raw)
Consider this C function:
int square(int i) { return i * i; }
How can we construct this from within Python using libgccjit?
First we need to import the Python bindings to libgccjit:
All state associated with compilation is associated with agccjit.Context:
ctxt = gccjit.Context()
The JIT library has a system of types. It is statically-typed: every expression is of a specific type, fixed at compile-time. In our example, all of the expressions are of the C int type, so let’s obtain this from the context, as a gccjit.Type:
int_type = ctxt.get_type(gccjit.TypeKind.INT)
The various objects in the API have reasonable __str__ methods:
Let’s create the function. To do so, we first need to construct its single parameter, specifying its type and giving it a name:
param_i = ctxt.new_param(int_type, b'i') print(param_i) i
Now we can create the function:
fn = ctxt.new_function(gccjit.FunctionKind.EXPORTED, ... int_type, # return type ... b"square", # name ... [param_i]) # params print(fn) square
To define the code within the function, we must create basic blocks containing statements.
Every basic block contains a list of statements, eventually terminated by a statement that either returns, or jumps to another basic block.
Our function has no control-flow, so we just need one basic block:
block = fn.new_block(b'entry') print(block) entry
Our basic block is relatively simple: it immediately terminates by returning the value of an expression. We can build the expression:
expr = ctxt.new_binary_op(gccjit.BinaryOp.MULT, ... int_type, ... param_i, param_i) print(expr) i * i
This in itself doesn’t do anything; we have to add this expression to a statement within the block. In this case, we use it to build a return statement, which terminates the basic block:
block.end_with_return(expr)
OK, we’ve populated the context. We can now compile it:
jit_result = ctxt.compile()
and get a gccjit.Result.
We can now look up a specific machine code routine within the result, in this case, the function we created above:
void_ptr = jit_result.get_code(b"square")
We can now use ctypes.CFUNCTYPE to turn it into something we can call from Python:
import ctypes int_int_func_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int) callable = int_int_func_type(void_ptr)
It should now be possible to run the code:
Options¶
To get more information on what’s going on, you can set debugging flags on the context using gccjit.Context.set_bool_option().
Setting gccjit.BoolOption.DUMP_INITIAL_GIMPLE will dump a C-like representation to stderr when you compile (GCC’s “GIMPLE” representation):
ctxt.set_bool_option(gccjit.BoolOption.DUMP_INITIAL_GIMPLE, True) jit_result = ctxt.compile() square (signed int i) { signed int D.260;
entry: D.260 = i * i; return D.260; }
We can see the generated machine code in assembler form (on stderr) by setting gccjit.BoolOption.DUMP_GENERATED_CODE on the context before compiling:
ctxt.set_bool_option(gccjit.BoolOption.DUMP_GENERATED_CODE, True) jit_result = ctxt.compile() .file "fake.c" .text .globl square .type square, @function square: .LFB6: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -4(%rbp) .L14: movl -4(%rbp), %eax imull -4(%rbp), %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE6: .size square, .-square .ident "GCC: (GNU) 4.9.0 20131023 (Red Hat 0.2-0.5.1920c315ff984892399893b380305ab36e07b455.fc20)" .section .note.GNU-stack,"",@progbits
By default, no optimizations are performed, the equivalent of GCC’s-O0 option. We can turn things up to e.g. -O3 by callinggccjit.Context.set_int_option() withgccjit.IntOption.OPTIMIZATION_LEVEL:
ctxt.set_int_option(gccjit.IntOption.OPTIMIZATION_LEVEL, 3) jit_result = ctxt.compile() .file "fake.c" .text .p2align 4,,15 .globl square .type square, @function square: .LFB7: .cfi_startproc .L16: movl %edi, %eax imull %edi, %eax ret .cfi_endproc .LFE7: .size square, .-square .ident "GCC: (GNU) 4.9.0 20131023 (Red Hat 0.2-0.5.1920c315ff984892399893b380305ab36e07b455.fc20)" .section .note.GNU-stack,"",@progbits
Naturally this has only a small effect on such a trivial function.
Full example¶
Here’s what the above looks like as a complete program:
import ctypes
import gccjit
def create_fn():
Create a compilation context:
ctxt = gccjit.Context()
Turn these on to get various kinds of debugging:
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_GENERATED_CODE, True)
Adjust this to control optimization level of the generated code:
if 0: ctxt.set_int_option(gccjit.IntOption.OPTIMIZATION_LEVEL, 3)
int_type = ctxt.get_type(gccjit.TypeKind.INT)
Create parameter "i":
param_i = ctxt.new_param(int_type, b'i')
Create the function:
fn = ctxt.new_function(gccjit.FunctionKind.EXPORTED, int_type, b"square", [param_i])
Create a basic block within the function:
block = fn.new_block(b'entry')
This basic block is relatively simple:
block.end_with_return( ctxt.new_binary_op(gccjit.BinaryOp.MULT, int_type, param_i, param_i))
Having populated the context, compile it.
jit_result = ctxt.compile()
This is what you get back from ctxt.compile():
assert isinstance(jit_result, gccjit.Result)
return jit_result
def test_calling_fn(i): jit_result = create_fn()
Look up a specific machine code routine within the gccjit.Result,
in this case, the function we created above:
void_ptr = jit_result.get_code(b"square")
Now use ctypes.CFUNCTYPE to turn it into something we can call
from Python:
int_int_func_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int) code = int_int_func_type(void_ptr)
Now try running the code:
return code(i)
if name == 'main': print(test_calling_fn(5))