mirror of
https://git.savannah.gnu.org/git/guile.git
synced 2025-06-10 14:00:21 +02:00
Comments in the JIT
* libguile/jit.c: Add some overview comments.
This commit is contained in:
parent
01c14512d6
commit
01a79bed89
1 changed files with 89 additions and 0 deletions
|
@ -41,11 +41,100 @@
|
|||
|
||||
|
||||
|
||||
/* Guile's just-in-time (JIT) compiler is a simple "template JIT". It
|
||||
produces machine code corresponding to each VM instruction,
|
||||
substituting in the arguments from the bytecode. The generated code
|
||||
performs the same operations on the Guile program state the VM
|
||||
interpreter would: the same stack reads and writes, the same calls,
|
||||
the same control flow: the same thing. It's a very simple JIT.
|
||||
|
||||
This JIT uses GNU Lightning, a library for generating assembly code.
|
||||
It has backends for every architecture you can think of. Lightning
|
||||
exposes a minimum of 3 "volatile" or "scratch" registers, those that
|
||||
may be overwritten by called functions, and 3 "non-volatile" or
|
||||
"preserved" registers, those whose values will persist over calls.
|
||||
Guile's JIT uses two preserved registers for itself, to store the
|
||||
current thread and the current stack pointer. The other four
|
||||
registers are available for the JIT. However as Guile's JIT is
|
||||
really simple and doesn't do register allocation, no other register
|
||||
is live between bytecodes; the other four registers are just scratch
|
||||
space.
|
||||
|
||||
Machine code emitted by the JIT (mcode) should only ever be entered
|
||||
from the interpreter (the VM). To enter bytecode, the interpreter
|
||||
calls an "entry trampoline" that saves the needed non-volatile
|
||||
registers, reserves some stack space, loads the thread and stack
|
||||
pointer into the reserved registers, then jumps into the mcode. The
|
||||
mcode then does its thing.
|
||||
|
||||
When mcode needs to call out to another function, e.g. via the "call"
|
||||
instruction, it makes a new frame in just the same way the VM would,
|
||||
with the difference that it also sets the machine return address
|
||||
(mRA) in the stack frame, in addition to the virtual (bytecode)
|
||||
return address (vRA). If the callee has mcode, then the caller jumps
|
||||
to the callee's mcode. It's a jump, not a call, as the stack is
|
||||
maintained on the side: it's not the stack used by the e.g. x86
|
||||
"call" instruction.
|
||||
|
||||
When mcode calls a function that doesn't have vcode, or returns to a
|
||||
continuation that doesn't have vcode, the mcode simply returns to the
|
||||
VM interpreter, allowing the interpreter to pick up from there. The
|
||||
return actually happens via an exit trampoline, which restores the
|
||||
saved register values.
|
||||
|
||||
Every function in Guile's VM begins with an "instrument-entry"
|
||||
instruction. The instruction links to a statically allocated "struct
|
||||
scm_jit_function_data" corresponding to that function. When the
|
||||
interpreter sees instrument-entry, first it checks that if the
|
||||
function has mcode, by looking in the scm_jit_function_data. If it
|
||||
has mcode, the interpreter enters mcode directly, as described above.
|
||||
|
||||
If a function doesn't have mcode, "instrument-entry" will increment a
|
||||
counter in the scm_jit_function_data. If the counter exceeds a
|
||||
threshold, the interpreter will ask the JIT compiler to produce
|
||||
mcode. If the JIT compiler was able to do so (always possible except
|
||||
in case of resource exhaustion), then it sets the mcode pointer in
|
||||
the scm_jit_function_data, and returns the mcode pointer to the
|
||||
interpreter. At that point the interpreter will enter mcode.
|
||||
|
||||
If the counter value does not exceed the threshold, then the VM
|
||||
will interpret the function instead of running compiled code.
|
||||
|
||||
Additionally, Guile puts an "instrument-loop" instruction into the
|
||||
body of each loop iteration. It works similarly, except that the
|
||||
returned mcode pointer starts in the middle of the function, at the
|
||||
point that corresponds to the program point of the "instrument-loop"
|
||||
instruction. The idea is that some functions have long-running loops
|
||||
in them, and it would be a shame to have to wait until the next time
|
||||
they're called to enter mcode. Being able to "tier up" from inside a
|
||||
loop reduces overall program latency.
|
||||
|
||||
Think of the JIT as microarchitecture. The interpreter specifies the
|
||||
architecture of the VM, in terms of the stack, stack and frame
|
||||
pointers, and a virtual instruction pointer. Sometimes this
|
||||
architectural state is manipulated by the interpreter. Sometimes
|
||||
it's compiled down to native code. But the existence of native code
|
||||
is a detail that's fully encapsulated; systems-oriented Guile Scheme
|
||||
can walk stacks, throw errors, reinstate partial continuations, and
|
||||
so on without being aware of the existence of the JIT. */
|
||||
|
||||
|
||||
|
||||
|
||||
/* Entry trampoline: saves registers, initializes THREAD and SP
|
||||
registers, and jumps into mcode. */
|
||||
static void (*enter_mcode) (scm_thread *thread, const uint8_t *mcode);
|
||||
|
||||
/* Exit trampoline: restores registers and returns to interpreter. */
|
||||
static void *exit_mcode;
|
||||
|
||||
/* Handle interrupts trampoline: the slow path of the handle-interrupts
|
||||
instruction, compiled as a stub on the side to reduce code size. */
|
||||
static void *handle_interrupts_trampoline;
|
||||
|
||||
static void compute_mcode (scm_thread *, struct scm_jit_function_data *);
|
||||
|
||||
/* State of the JIT compiler for the current thread. */
|
||||
struct scm_jit_state {
|
||||
jit_state_t *jit;
|
||||
scm_thread *thread;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue