Safari's FTL JIT Compiler: Optimizing Code Execution

This article explores how the Fourth Tier LLVM (FTL) Just-In-Time (JIT) compiler optimizes JavaScript execution within the Safari web browser's JavaScriptCore engine. It covers the multi-tiered compilation pipeline, how the engine identifies and promotes frequently executed code, the architectural shift from the LLVM backend to the Bare Bones Backend (B3), and the specific low-level optimization techniques used to transform dynamic JavaScript into highly efficient native machine code.

The Four-Tier Execution Pipeline

Safari’s JavaScript engine, JavaScriptCore (JSC), does not compile all code at the highest optimization level immediately, as doing so would cause massive delays when loading web pages. Instead, it uses a four-tier pipeline to balance startup speed with peak execution performance:

  1. Low-Level Interpreter (LLInt): Executes bytecode immediately with zero compilation delay.
  2. Baseline JIT: Compiles functions that run multiple times into basic machine code while collecting profiling data (such as variable types).
  3. Data Flow Graph (DFG) JIT: Uses the profiling data from the Baseline JIT to generate highly optimized code based on speculative assumptions.
  4. FTL JIT: Reserved for the "hottest" code—functions and loops that run thousands of times. It spends more time compiling to generate the fastest possible machine code.

From LLVM to the Bare Bones Backend (B3)

When Apple first introduced the FTL tier, the acronym stood for "Fourth Tier LLVM." The engine translated JavaScript into an Intermediate Representation (IR) and fed it into the LLVM compiler infrastructure to apply C-level optimizations.

However, LLVM was designed for ahead-of-time compilation, meaning its compile times were often too slow for the real-time demands of a web browser. To solve this, WebKit engineers replaced LLVM with a custom low-level optimizer called the Bare Bones Backend (B3) and retroactively changed the FTL acronym to mean "Faster Than Light." B3 delivers the same high-quality machine code as LLVM but reduces compilation time and memory overhead by stripping out features unnecessary for JavaScript.

Type Speculation and OSR Exits

JavaScript is a dynamically typed language, meaning a variable can hold an integer in one moment and a string the next. Checking variable types before every operation wastes CPU cycles.

The FTL compiler bypasses this by using type speculation. Relying on the profiling data gathered by the Baseline and DFG tiers, the FTL assumes variables will continue to be the same type they have been in the past. It then strips away all type-checking logic and compiles math and memory operations directly for those assumed types.

If the JavaScript code suddenly introduces an unexpected data type, the FTL's assumption fails. When this happens, the FTL triggers an On-Stack Replacement (OSR) exit. The engine pauses execution, safely reconstructs the state of the program, and drops the code back down to the Baseline JIT to handle the new type, preventing the browser from crashing.

High-Level and Low-Level Optimizations

The FTL pipeline merges the high-level optimizations of the DFG compiler with the low-level optimizations of the B3 backend. When code reaches the FTL, it undergoes several aggressive transformations:

  • Function Inlining: The FTL replaces a function call with the actual body of the called function. This removes the overhead of setting up a new call stack and allows the compiler to optimize the combined code as a single unit.
  • Loop-Invariant Code Motion (LICM): If the FTL detects a calculation inside a loop that produces the exact same result on every iteration, it moves that calculation outside the loop so it only runs once.
  • Dead Code Elimination: Any code paths or variable assignments that the compiler proves will never affect the final output are completely removed before machine code generation.
  • Escape Analysis: The FTL analyzes where objects are used. If a new object is created inside a function and never leaves (escapes) that function, the FTL skips allocating memory on the heap and instead stores the object's properties directly in fast CPU registers.

Register Allocation and Machine Code Generation

In the final stage of the FTL pipeline, the B3 backend translates the optimized intermediate representation into instructions specific to the device's CPU architecture (such as ARM64 for Apple Silicon or x86_64 for Intel).

A critical part of this final step is register allocation. Fetching data from the computer's main memory (RAM) is slow. The FTL uses complex algorithms to map the most frequently used JavaScript variables directly to the CPU's physical hardware registers. By keeping active data inside the CPU itself and executing streamlined instructions, the FTL allows dynamic JavaScript in Safari to run at speeds approaching natively compiled C++ applications.