Inside JavaScriptCore: LLInt and FTL JIT Execution
JavaScriptCore (JSC), the JavaScript engine powering WebKit and Safari, uses a multi-tier execution pipeline designed to deliver instant startup speeds while maximizing long-term execution throughput. The execution lifecycle starts in the Low-Level Interpreter (LLInt), which interprets generated bytecode instantly without compilation overhead while gathering type profiles. As execution counters identify frequently run code paths, JSC promotes this “hot” code through intermediate compilation tiers until it reaches the Faster Than Light (FTL) JIT, an optimizing compiler that generates highly specialized, native machine code using low-level backend optimizations.
The Compilation Pipeline Overview
JavaScriptCore evaluates source code by parsing it into an Abstract Syntax Tree (AST) and generating a platform-independent bytecode format. To balance low latency and peak performance, JSC employs four distinct execution tiers:
- LLInt (Low-Level Interpreter): Immediate execution with zero compilation latency.
- Baseline JIT: Fast template-based native code generation.
- DFG (Data Flow Graph) JIT: Speculative, profile-guided optimization.
- FTL (Faster Than Light) JIT: Deep, low-level native compilation for peak performance.
Execution with the Low-Level Interpreter (LLInt)
The LLInt is the entry point for all JavaScript code in JSC. Its primary objective is to begin executing instructions immediately while keeping memory overhead minimal.
Architecture and Assembly Generation
Unlike traditional interpreters written in C++ that use giant
switch statements, the LLInt is written in an
engine-specific assembly dialect called offlineasm.
- Direct Machine Code Dispatch: Offlineasm compiles ahead of time into compact, native assembly instructions for target architectures (x86-64, ARM64, etc.).
- Register Calling Conventions: The LLInt operates directly on CPU registers and native stack frames, mirroring the calling conventions of native code. This design makes transitions between the interpreter and the JIT compiler tiers extremely efficient.
Profiling and Tier-Up Triggers
While executing bytecode, the LLInt performs runtime profiling: * Value Profiling: Tracks observed JavaScript types (integers, doubles, strings, object shapes/structures) passing through instructions. * Execution Counters: Tracks how many times a function is called and how often loops execute.
When an execution counter crosses a predefined threshold, the LLInt initiates On-Stack Replacement (OSR) to elevate the function to the Baseline JIT and subsequently into optimizing tiers.
The Intermediate Stage: DFG JIT
Before code can reach the FTL tier, it must pass through the Data Flow Graph (DFG) JIT. The DFG analyzes the type data collected by the LLInt and Baseline JIT. It constructs a control flow and data flow graph, applies speculative optimizations (assuming types will remain consistent), and removes runtime checks that it proves redundant. If a function remains heavily used after DFG compilation, it becomes a candidate for the FTL JIT.
Deep Optimization with the FTL JIT
The FTL JIT is JavaScriptCore’s highest-tier compiler. It takes the optimized intermediate representation (IR) from the DFG and subjects it to advanced compiler transformations to produce assembly code comparable to compiled C or C++.
Backend Pipeline: The B3 Compiler
FTL compiles code using the B3 (Bare Bones Backend), a low-level optimizing compiler designed specifically for dynamic languages.
- DFG-to-B3 Lowering: High-level DFG IR nodes are lowered into B3 IR, which represents operations close to bare metal (register operations, memory offsets, and bitwise manipulations).
- Low-Level Optimizations: B3 performs classic
compiler optimizations, including:
- Global Common Subexpression Elimination (CSE)
- Dead code elimination and loop invariant code motion
- Advanced register allocation (Iterated Register Coalescing)
- Precise instruction selection tailored to modern CPU architectures
Speculative Specialization
FTL leverages the type profiles gathered since the LLInt stage to
specialize operations. For example, a polymorphic addition operation
(+) is reduced to a single native CPU addition instruction
if the operands are proven to always be 32-bit integers.
Handling Invalidation: On-Stack Replacement (OSR) Exit
Because JavaScript is dynamic, speculative assumptions made by the FTL JIT may occasionally be violated at runtime (for example, if a function that always received integers suddenly receives a floating-point number or an object).
- Guard Checks: The FTL compiler inserts lightweight guard checks before specialized operations.
- OSR Exit: If a guard check fails, execution immediately bails out of the FTL machine code. The execution state (CPU registers and stack frames) is reconstructed to match the layout expected by the DFG or Baseline tiers, and execution resumes seamlessly without interrupting the application.
Summary
The synergy between the LLInt and the FTL JIT enables JavaScriptCore to meet two competing demands: immediate responsiveness and maximum compute performance. The LLInt provides instant startup and essential profiling data, while the FTL JIT leverages that data to produce optimized machine code for long-running, performance-critical code paths.