src/jit.c - JIT


JIT (Just In Time) compilation converts bytecode to native machine code instructions and executes the generated instruction sequence directly.

Actually it's not really just in time, it's just before this piece of code is used and not per subroutine or even opcode, it works per bytecode segment.

Functions ^

static void insert_fixup_targets(Interp *interpreter, char *branch, size_t limit)

Look at fixups, mark all fixup entries as branch target.

TODO: actually this is wrong: fixups belong only to one code segment. The code below doesn't check, for which segments the fixups are inserted.

static void make_branch_list(Interp *interpreter, Parrot_jit_optimizer_t *optimizer, opcode_t *cur_op, opcode_t *code_start, opcode_t *code_end)

optimizer->map_branch parallels the opcodes with a list of branch information and register mapping information

branch instructions have JIT_BRANCH_SOURCE

opcodes jumped to have JIT_BRANCH_TARGET

mapped arguments have register type + 1 and finally

after register allocation these have the processor register that got mapped

static void set_register_usage(Interp *interpreter, Parrot_jit_optimizer_t *optimizer, Parrot_jit_optimizer_section_ptr cur_section, op_info_t *op_info, opcode_t *cur_op, opcode_t *code_start)

Sets the register usage counts.

static void make_sections(Interp *interpreter, Parrot_jit_optimizer_t *optimizer, opcode_t *cur_op, opcode_t *code_start, opcode_t *code_end)

I386 has JITed vtables, which have the vtable# in extcall.

This Parrot_jit_vtable_n_op() does use register mappings.

static void make_branch_targets(Interp *interpreter, Parrot_jit_optimizer_t *optimizer, opcode_t *code_start)

Makes the branch targets.

static void sort_registers(Interp *interpreter, Parrot_jit_optimizer_t *optimizer, opcode_t *code_start)

Sorts the Parrot registers prior to mapping them to actual hardware registers.

static void assign_registers(Interp *interpreter, Parrot_jit_optimizer_t *optimizer, Parrot_jit_optimizer_section_ptr cur_section, opcode_t *code_start, int from_imcc)

Called by map_registers() to actually assign the Parrot registers to hardware registers.


Before actually assigning registers, we should optimize a bit:

1) calculate max use count of register types for all sections

2) calculate costs for register preserving and restoring for two different allocation strategies:

   a) allocate non-volatiles first
      overhead for jit_begin, jit_end:
      - 2 * max_used_non_volatile registers
      overhead for register preserving around non-jitted sections:
      - only used IN arguments are saved
      - only OUT non-volatile arguments are restored
   b) allocate volatiles first
      no overhead for jit_begin, jit_end
      overhead per JITed op that calls a C function:
      - 2 * n_used_volatiles_to_preserve for each call
      overhead for register preserving around non-jitted sections:
      - all volatiles are saved and restored around non-jitted sections
NB for all cost estimations size does matter: a 64bit double counts as two 32bit ints. Opcode count is assumed to be just one.

3) depending on costs from 2) use one of the strategies That does still not account for any usage patterns. Imcc has loop nesting depth, but that's not available here. OTOH smaller code tends to perform better because of better cache usage.

Usage analysis could show that a mixture of both strategies is best, e.g: allocate 2-4 non-volatiles and the rest from volatiles. But that would complicate the allocation code a bit.

static void map_registers(Interp *interpreter, Parrot_jit_optimizer_t *optimizer, opcode_t *code_start)

Maps the most used Parrot registers to hardware registers.

static void debug_sections(Interp *interpreter, Parrot_jit_optimizer_t *optimizer, opcode_t *code_start)

Prints out debugging info.

static Parrot_jit_optimizer_t *optimize_jit(Interp *interpreter, opcode_t *cur_op, opcode_t *code_start, opcode_t *code_end)

Called by build_asm() to run the optimizer.

static Parrot_jit_optimizer_t *optimize_imcc_jit(Interp *interpreter, opcode_t *cur_op, opcode_t *code_start, opcode_t *code_end, struct PackFile_Segment *jit_seg)

Generate optimizer stuff from the _JIT section in the packfile.

size_t reg_offs(Interp *interpreter, int typ, int i)

Returns the offset of register typ[i].

jit/arch/jit_emit.h has to define Parrot_jit_emit_get_base_reg_no(pc)

static void Parrot_jit_load_registers(Parrot_jit_info_t *jit_info, Interp *interpreter, int volatiles)

Load registers for the current section from parrot to processor registers. If volatiles is true, this code is used to restore these registers in JITted code that calls out to Parrot.

static void Parrot_jit_save_registers(Parrot_jit_info_t *jit_info, Interp *interpreter, int volatiles)

Save registers for the current section. If volatiles is true, this code is used to preserve these registers in JITted code that calls out to Parrot.

void Parrot_destroy_jit(void *ptr)

Frees the memory used by the JIT subsystem.

jit_f build_asm(Interp *interpreter, opcode_t *pc, opcode_t *code_start, opcode_t *code_end, void *objfile)

This is the main function of the JIT code generator.

It loops over the bytecode, calling the code generating routines for each opcode.

The information obtained is used to perform certain types of fixups on native code, as well as by the native code itself to convert bytecode program counters values to hardware program counter values.

Finally this code here is used to generate native executables (or better object files that are linked to executables), if EXEC_CAPABLE is defined. This functionality is triggered by

  parrot -o foo.o foo.imc
which uses the JIT engine to translate to native code inside the object file.

void Parrot_jit_newfixup(Parrot_jit_info_t *jit_info)

Remember the current position in the native code for later update.


include/parrot/jit.h, docs/jit.pod,d src/jit_debug.c, jit/$jitcpuarch/jit_emit.h, jit/$jitcpuarch/core.jit.