Skip to content

JIT Compilers

opencode-agent[bot] edited this page Sep 27, 2026 · 4 revisions

JIT Compilers

JNode features a tiered Just-In-Time (JIT) compiler pipeline for the x86 architecture, consisting of a fast, stack-based L1 compiler and an optimizing L2 compiler.

Overview

Unlike standard JVMs that execute bytecode via interpretation initially, JNode compiles all bytecode to native machine code before execution. There is no bytecode interpreter. To balance startup time and performance, JNode uses a tiered compilation strategy:

  1. L1 (Level 1) Compilers: Fast, non-optimizing compilers used for most methods to ensure quick startup.
  2. L2 (Level 2) Compiler: An optimizing compiler used for performance-critical methods.

The JIT compilers are written entirely in Java and run within the JNode VM itself. They translate VmByteCode into native x86 instructions which are then executed directly by the processor.

Compiler Tiers

L1A / L1B Compilers

  • Location: core/src/core/org/jnode/vm/x86/compiler/l1a/ and l1b/
  • Characteristics: Fast compilation, low memory footprint.
  • Mechanism: These compilers emulate the Java operand stack using a "Virtual Stack" (VirtualStack.java) or "Item Stack". They translate bytecodes almost directly into native x86 instructions without building an Intermediate Representation (IR) or performing complex register allocation. Registers are assigned greedily as items are pushed and popped.
  • Inlining: Both L1A and L1B support method inlining, driven by the OptimizingBytecodeVisitor and hinted by annotations like @Inline.

L2 Compiler

  • Location: core/src/core/org/jnode/vm/x86/compiler/l2/
  • Characteristics: Slower compilation, higher memory footprint, but generates highly optimized native code.
  • Mechanism: The L2 compiler is a modern, optimizing JIT compiler. Its pipeline involves:
    1. IR Generation: Translates bytecode into an Intermediate Representation (IRControlFlowGraph).
    2. SSA Construction: Converts the IR into Static Single Assignment form (cfg.constructSSA()).
    3. Optimization passes: Performs optimizations on the SSA graph (cfg.optimize(), cfg.removeUnusedVars()).
    4. De-SSA: Reverts SSA form (cfg.deconstrucSSA(), cfg.removeDefUseChains()).
    5. Register Allocation: Uses a Linear Scan Allocator (LinearScanAllocator.java) based on computed Live Ranges.
    6. Code Generation: Emits native x86 code from the optimized IR (X86CodeGenerator.java).

Key Components

Class / Package Role
org.jnode.vm.compiler Base classes for the compiler framework (CompiledMethod, CompilerBytecodeVisitor).
org.jnode.vm.x86.compiler x86-specific base classes and helpers (AbstractX86Compiler, X86CompilerHelper).
org.jnode.vm.compiler.ir Intermediate Representation used by the L2 compiler.
org.jnode.vm.x86.compiler.l2.LinearScanAllocator Register allocator for the L2 compiler.
org.jnode.assembler.x86 The x86 assembler framework (X86BinaryAssembler) used to emit raw bytes.

Boot-time Compilation vs. Runtime Compilation

  • Build/Boot Time: BootImageBuilder uses the L1 compiler running on the host JVM to pre-compile the core classes required for the system to boot. These compiled methods are baked into the binary boot image.
  • Runtime: When a class is loaded dynamically, or a method is executed for the first time (if lazily compiled), the VM invokes the JIT compiler running on the target OS.

Magic Helpers

The compilers interact deeply with JNode's VmMagic framework. MagicHelper classes within the compiler packages intercept calls to magic methods (like Unsafe.getAddress()) and directly emit the corresponding native instructions instead of emitting an actual method call. This is crucial for low-level performance.

Floating-Point Emission

core/src/core/org/jnode/vm/x86/compiler/X86CompilerHelper.java (1219 lines) is the shared emitter for x86 instruction sequences. Its most intricate part is the float→int conversion pair:

Method Line Purpose
emitF2I(X86Assembler os, GPR destReg, int destDisp) :988 float/double → int, JLS §5.1.3 correct (NaN→0, ±Inf→MAX/MIN, truncate toward zero)
emitF2L(X86Assembler os, GPR destReg, int destDisp) :1108 float/double → long, same rules

Both wrap a single FISTP in a temporary truncate x87 control word (the global one is round-to-nearest for strictfp), then classify the x87 "integer indefinite" sentinel 0x80000000 / 0x8000000000000000 and patch in the JLS-mandated constant. Both are called from l1a/l1b IntItem.popFromFPU / LongItem.popFromFPU, from X86BytecodeVisitor store-to-local sites, and from the l2 GenericX86CodeGenerator F2I cases.

The global x87 control word is set once by init_fpu in core/src/native/x86/cpu.asm to PC=10 (53-bit double) and RC=00 (nearest-even), giving CW = 0x027F. See X87-FPU-Support for the full bit layout, the emitted sequence, the SP-relative displacement fixup, and the label-uniquification rules.

Related Pages

  • X87-FPU-Support — x87 control word, emitF2I/emitF2L, JLS conversion, StrictMath argument reduction
  • L1-Compiler-Deep-Dive — Detailed walkthrough of the VirtualStack, Item state machine, register allocation, and optimization limits
  • L2-Compiler-Deep-Dive — SSA-based IR, linear-scan register allocator, optimization pipeline, and x86 code generation
  • VM-Magic — How magic annotations interact with the JIT.
  • Code-Conventions — Explains compiler-related annotations like @Inline and @NoOptCompilePragma.
  • Build-System — How BootImageBuilder pre-compiles code.

Clone this wiki locally