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:

  1. LLInt (Low-Level Interpreter): Immediate execution with zero compilation latency.
  2. Baseline JIT: Fast template-based native code generation.
  3. DFG (Data Flow Graph) JIT: Speculative, profile-guided optimization.
  4. 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.

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.

  1. 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).
  2. 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).

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.