JavaScriptCore Multi-Tier Execution: LLInt and DFG
JavaScriptCore, the JavaScript engine powering WebKit and Safari, achieves high performance and fast startup times through a sophisticated multi-tier execution pipeline. This article explores how JavaScriptCore orchestrates execution between its lowest tier, the Low-Level Interpreter (LLInt), and its optimizing mid-tier compiler, the Data Flow Graph (DFG) JIT. By balancing rapid startup via lightweight interpretation with speculative optimizations powered by runtime type profiling, JavaScriptCore dynamically accelerates hot code paths while minimizing memory and compilation overhead.
The Multi-Tier Execution Pipeline
JavaScriptCore uses a four-tier architecture designed to execute code immediately and selectively optimize functions that run frequently. The tiers are:
- Low-Level Interpreter (LLInt): Immediate execution with zero compilation latency.
- Baseline JIT: Fast, template-based compilation for lightly used code.
- Data Flow Graph (DFG) JIT: Optimizing JIT using type speculation and control-flow analysis.
- Faster Than Light (FTL) JIT: High-level optimization utilizing LLVM or the B3 backend for long-running code.
While the Baseline JIT often acts as an intermediate step, the transition dynamics between the LLInt and the DFG JIT demonstrate the core principles of how JavaScriptCore manages profiling, tier-up triggers, and speculative compilation.
The Low-Level Interpreter (LLInt)
The LLInt is written in a low-level domain-specific assembly language called offlineasm. It operates directly on JavaScriptCore bytecode without requiring a traditional Just-In-Time compilation step.
- Instant Startup: The LLInt executes code with near-zero latency because it requires no machine-code generation phase.
- Low Memory Footprint: Because it interprets compact bytecode instructions, the LLInt keeps the initial memory footprint minimal.
- Profiling and Metrics Collection: As the LLInt
executes bytecode, it updates execution counters and gathers type
feedback using inline profiling structures like
ValueProfileandArrayProfile. These profiles record the observed runtime types of variables and object shapes.
Tiering Up: Triggering Compilation
JavaScriptCore uses execution counters embedded within functions and loops to monitor code execution frequency. When a function or loop crosses predefined execution thresholds in the LLInt or Baseline tier:
- Counter Evaluation: Every function entry and loop backedge increments a tier-up counter.
- Heuristic Evaluation: When the counter reaches its threshold, the engine evaluates whether the collected profiling data is stable enough to justify compilation.
- Triggering the DFG: If the criteria are met, the engine invokes the DFG JIT in a background thread to compile the target function or loop.
The Data Flow Graph (DFG) JIT
The DFG JIT is an optimizing compiler that converts bytecode into a high-level intermediate representation (IR) structured as a directed graph representing data flow.
- Speculative Optimization: Using the type profiles
gathered by the LLInt and Baseline tiers, the DFG assumes that variables
will continue to have the same types they had previously. For example,
if a
+operation has only ever received integers, the DFG compiles it into a fast, native integer addition rather than a polymorphic operation. - Graph-Based Optimizations: The DFG performs standard compiler optimizations, including dead-code elimination, common subexpression elimination, constant folding, and inlining of small functions.
- Control-Flow Analysis: The DFG optimizes loop invariants and reduces redundant type checks across basic blocks.
On-Stack Replacement and Deoptimization
Transitions between execution tiers are managed through On-Stack Replacement (OSR):
- OSR Entry: Once the DFG finishes compiling a function or hot loop, JavaScriptCore uses OSR Entry to transition execution directly from the interpreter or baseline frame to the newly compiled DFG machine code, even mid-loop.
- OSR Exit (Deoptimization): Because DFG optimizations are speculative, type assumptions may be violated at runtime (e.g., passing a string into a function previously optimized exclusively for integers). When a type check fails, the DFG executes an OSR Exit. The execution state is reconstructed into an unoptimized frame, and control is transferred back to the LLInt or Baseline JIT to resume execution safely and collect updated type information.