Repository navigation
L1 Compiler
L1 compiler register allocation and virtual stack management for fast JIT compilation.
The L1 compiler (org.jnode.vm.x86.compiler.l1a) uses a virtual stack (VirtualStack) and register pools (X86RegisterPool) to translate bytecode to native x86 code. Rather than immediately emitting instructions, values are held as Item objects on a virtual stack, keeping them in registers as long as possible. This delayed-emission strategy maximizes register utilization within basic blocks, trading aggressive optimization for fast compilation speed.
| Class / File | Role |
|---|---|
core/src/core/org/jnode/vm/x86/compiler/l1a/X86RegisterPool.java |
Register pool management for GPR and XMM registers |
core/src/core/org/jnode/vm/x86/compiler/l1a/VirtualStack.java |
Virtual operand stack with delayed emission |
core/src/core/org/jnode/vm/x86/compiler/l1a/Item.java |
Abstract base for stack values (constant, GPR, XMM, STACK, LOCAL) |
core/src/core/org/jnode/vm/x86/compiler/l1a/WordItem.java |
Single-register items (INT, FLOAT, REFERENCE) |
core/src/core/org/jnode/vm/x86/compiler/l1a/DoubleWordItem.java |
Two-register items (LONG, DOUBLE) in 32-bit mode |
The X86RegisterPool manages available x86 registers in groups. Each RegisterGroupUsage instance represents a group of aliases (e.g., EAX/RAX for different JVM types) and tracks ownership:
// GPRs32 pools: EAX, EDX, ECX, ESI, EBX (EDI reserved for statics)
public static final class GPRs32 extends X86RegisterPool {
protected RegisterGroupUsage[] initialize() {
return new RegisterGroupUsage[]{
new RegisterGroupUsage(
new RegisterEntry(X86Register.EAX, JvmType.INT),
new RegisterEntry(X86Register.EAX, JvmType.REFERENCE),
new RegisterEntry(X86Register.EAX, JvmType.FLOAT), false),
// ... EDX, ECX, ESI, EBX
};
}
}32-bit GPR priority (highest cost first): EBX < ESI < ECX < EDX < EAX
64-bit GPR priority (highest cost first): R15 < R14 < R13 < R12 < R10 < R9 < R8 < RSI < RBX < RCX < RDX < RAX
Values transition through states tracked by Item.Kind:
CONSTANT ──load()──> GPR ──push()──> STACK
↑
└──load()───┘
-
load(ec): Brings value into a GPR (requests from pool, spills if necessary) -
push(ec): Emits code to push value onto the actual stack (releases GPR) -
spill(ec, reg): Spills a GPR to make room (transfers to STACK kind) -
release(ec): Frees resources without emission
The VirtualStack maintains the operand stack as Item objects:
void push(Item item) {
if (tos == stack.length) growStack();
stack[tos++] = item;
}
Item pop() {
tos--;
return stack[tos];
}Type-safe pops validate JVM types:
final IntItem popInt() { return (IntItem) pop(JvmType.INT); }
final LongItem popLong() { return (LongItem) pop(JvmType.LONG); }
final RefItem popRef() { return (RefItem) pop(JvmType.REFERENCE); }
final FloatItem popFloat() { return (FloatItem) pop(JvmType.FLOAT); }
final DoubleItem popDouble() { return (DoubleItem) pop(JvmType.DOUBLE); }32-bit mode: LONG and DOUBLE occupy two registers (LSB + MSB). DoubleWordItem tracks both registers:
final X86Register.GPR32 getLsbRegister(EmitterContext ec) { return lsb; }
final X86Register.GPR32 getMsbRegister(EmitterContext ec) { return msb; }64-bit mode: LONG and DOUBLE use single GPR64 registers (e.g., RAX).
Floating-point values use SSE XMM registers:
| Mode | Registers |
|---|---|
32-bit (XMMs32) |
XMM0–XMM7 |
64-bit (XMMs64) |
XMM0–XMM15 |
| Register | Usage |
|---|---|
| EAX/RAX | Return values (int/ref), implicit for add/sub operands |
| ECX | Shift amount for ishr/lshr |
| EDX:EAXX | 32-bit long return value |
| EDI | Points to statics (reserved) |
| R12 | Points to current VmProcessor (reserved) |
The L1 compiler reuses X86BytecodeVisitor instances across method compilations via ThreadLocal. When a compilation fails (e.g., unresolvable class), registers can remain allocated and items can remain on the virtual stack. Without cleanup, the next method compilation would fail with Error("Register(s) in use") in X86RegisterPool.reset().
Recovery chain:
-
NativeCodeCompiler.doCompile()tracks success via aboolean compiledflag. On failure, callscleanupBytecodeVisitor(bcv)beforereleaseBytecodeVisitor(bcv). -
X86Level1ACompiler.cleanupBytecodeVisitor()overrides the hook. Unwraps the visitor chain (Optimizing → Verifying →X86BytecodeVisitor) and callscleanupAfterFailedCompilation(). -
X86BytecodeVisitor.cleanupAfterFailedCompilation()force-resets GPR pool, XMM pool, and virtual stack. -
X86RegisterPool.forceReset()releases each allocated register with a warning log, then resets the pool index (unlikereset()which throws on in-use registers).
| Class | Method | Role |
|---|---|---|
NativeCodeCompiler |
cleanupBytecodeVisitor() |
No-op hook for subclasses |
X86Level1ACompiler |
cleanupBytecodeVisitor() |
Unwraps visitor chain, triggers cleanup |
X86Level1ACompiler |
unwrapBytecodeVisitor(visitor) |
Walks the whole DelegatingCompilerBytecodeVisitor chain; returns null if it does not end in an X86BytecodeVisitor
|
X86BytecodeVisitor |
cleanupAfterFailedCompilation() |
Resets all pools and vstack |
X86RegisterPool |
forceReset() |
Releases allocated registers with warning |
NativeCodeCompiler.doCompile wraps every visitor that is not already a VerifyingCompilerBytecodeVisitor into one. X86Level1ACompiler.createBytecodeVisitor deliberately returns the bare X86BytecodeVisitor when inlineMethods == false — it only adds new OptimizingBytecodeVisitor(entryPoints, cbv, loader) on the inlining path. So by the time doCompile finishes wrapping, the object that reaches releaseBytecodeVisitor is a VerifyingCompilerBytecodeVisitor wrapper, never the raw visitor.
The old release path assumed a single-level OptimizingBytecodeVisitor it could unwrap with one getDelegate():
X86BytecodeVisitor bv;
if (inlineMethods) {
bv = (X86BytecodeVisitor) ((OptimizingBytecodeVisitor) visitor).getDelegate();
} else {
bv = (X86BytecodeVisitor) visitor; // CCE on every method with inlining off
}With inlineMethods == false that else branch cast the wrapper directly and threw ClassCastException on every compiled method, so nothing was ever returned to the ThreadLocal pool. The fix (commit 81bfac7c3, closing #743) uses the same helper cleanupBytecodeVisitor already relied on:
@Override
protected synchronized void releaseBytecodeVisitor(CompilerBytecodeVisitor visitor) {
final X86BytecodeVisitor bv = unwrapBytecodeVisitor(visitor);
if (bv != null) {
byteCodeVisitorHolder.set(bv);
}
}unwrapBytecodeVisitor walks the entire delegating chain and returns null rather than throwing, so a chain that does not end in an X86BytecodeVisitor simply skips pooling. The regression test is core/src/test/org/jnode/test/CoreTestSuite.java-reachable X86Level1ACompilerVisitorTest (172 lines), which exercises release with inlining on and off and the null-terminating chain.
-
Basic block barriers: Virtual stack resets at each basic block start. All pending items flush to real stack, limiting register utilization across branches.
-
Aliasing restriction: Modifying a value on the vstack is forbidden.
loadLocal()pins any aliased stack items into registers before store. -
ECX for shifts: Non-constant shift amounts must be in ECX. The compiler explicitly requests ECX for variable shifts.
-
Register priority: Lower-indexed registers have higher allocation cost. EAX is cheapest in 32-bit mode, R15 is cheapest in 64-bit.
-
FPU stack discipline: FPU operations use a separate 8-slot
FPUStack. Items must be on top before fxch/fstp operations. -
64-bit constants: 64-bit instructions still take 32-bit immediates. Loading large constants requires MOV to register first.
-
Constant-folding item leaks: The
ItemFactorytracks item creation/release balance viaCHECK_BALANCED_ITEM_FACTORY. When folding constants (e.g.,ineg,i2l,lneg), the old constant must be released after creating the new constant. Prior to the fix,ineg,i2l, andlnegconstant-folding paths leaked items, causing unbalanced warnings. The fix addsval.release(eContext)afterifac.createIConst()in each constant-folding branch. The warning now includes the leak count for diagnostics. -
SSE constant folding for sub/mul/div:
FPCompilerSSEpreviously folded all SSE binary operations as addition (fpv1 + fpv2). The fix dispatches by operation type (SSE_SUB,SSE_MUL,SSE_DIV,SSE_ADD) and releases both operand items after folding. -
Never unwrap a compiler visitor with a single
getDelegate(). The wrapper count depends on the inlining path, so the only safe form is awhile (v instanceof DelegatingCompilerBytecodeVisitor)loop. See "Visitor Unwrapping and the Pooling Round-Trip". -
Never call
complete()on a freshly constructed helperArgument.complete()requires the argument to be bound to anArgumentBundle; a helper built on the fly must call the protecteddoCompleteinstead. This is theURLArgumentfile:-scheme bug. See Shell-Syntax.
- JIT-Compilers
- L1-Compiler-Deep-Dive
- L2-Compiler-Deep-Dive
- Stack-Frame-Layout
-
Core-Test-Suite — the host suite that runs
X86Level1ACompilerVisitorTest