Balancing Speed and Optimization in Safari's DFG JIT

This article explores how the Data Flow Graph (DFG) Just-In-Time (JIT) compiler in Safari’s JavaScriptCore engine strikes a critical balance between rapid compilation times and high-speed code execution. By operating as an intermediate tier within a multi-tiered architecture, utilizing dynamic type speculation, and employing On-Stack Replacement (OSR), the DFG JIT optimizes frequently executed JavaScript code without causing noticeable delays for the user.

The Multi-Tiered Execution Model

To understand how the DFG JIT balances speed and optimization, it is necessary to look at its position within JavaScriptCore (JSC), the engine that powers Safari. JSC uses a four-tier execution model:

  1. LLInt (Low-Level Interpreter): Starts executing code immediately with zero compilation delay.
  2. Baseline JIT: Compiles code quickly with minimal optimization once it runs a few times.
  3. DFG JIT: Steps in when code becomes "warm" (executed dozens or hundreds of times).
  4. FTL (Faster Than Light) JIT: An aggressive, heavy-weight optimizing compiler for the "hottest" code.

The DFG JIT achieves its balance by refusing to compile everything. It relies on the LLInt and Baseline JIT to execute code initially. By the time a function reaches the DFG tier, JSC knows the code is performance-critical, justifying the CPU time required to compile and optimize it.

Profiling and Speculative Compilation

JavaScript is dynamically typed, meaning a variable can hold a number at one moment and a string the next. Compiling every possible type combination into machine code takes too long and results in slow execution.

The DFG JIT solves this through speculative compilation. While code runs in the LLInt and Baseline tiers, JSC collects profiling data about the types of variables passing through functions. When the DFG JIT takes over, it reads this profile and makes an educated guess (speculation).

If a variable has only ever been an integer, the DFG JIT compiles the code assuming it will always be an integer. This drastically reduces compilation time because the compiler completely ignores the complex logic required to handle strings, objects, or floating-point numbers for that specific operation. The resulting machine code is compact and executes rapidly.

On-Stack Replacement (OSR)

Because the DFG JIT relies on assumptions, it needs a safety net for when those assumptions are wrong. This is handled via On-Stack Replacement (OSR).

Instead of compiling slow fallback paths for unexpected variable types, the DFG JIT inserts lightweight type checks. If a check fails during execution (for example, an expected integer suddenly becomes an object), the DFG triggers an OSR Exit. It immediately halts execution of the optimized code, reconstructs the program state, and drops back down to the Baseline JIT.

This mechanism allows the DFG to be aggressively optimistic. It saves compilation time by only generating code for the most likely scenarios, and it maximizes execution speed by keeping the compiled pathways free of bloated, generalized instructions. Once the Baseline JIT gathers new profiling data about the unexpected type, the DFG can recompile the function with updated assumptions.

Graph-Based Optimizations

When translating bytecode into machine code, the DFG JIT converts the instructions into a Data Flow Graph. This representation explicitly maps out how data moves from one operation to another, allowing the compiler to quickly perform high-impact optimizations without doing exhaustive, time-consuming code analysis.

Key optimizations performed at this stage include:

  • Inlining: The DFG takes small, frequently called functions and copies their bodies directly into the calling function. This eliminates the overhead of function calls and allows the compiler to optimize the combined code as a single unit.
  • Dead Code Elimination: If the data flow reveals that a calculated value is never used, the DFG deletes the instructions that generate that value.
  • Common Subexpression Elimination: If the same calculation is performed multiple times with the same inputs, the DFG computes it once and reuses the result.
  • Escape Analysis: The DFG analyzes whether a newly created object ever leaves the scope of the current function. If it does not, the compiler avoids slow memory allocations on the heap and places the object's data directly into fast CPU registers.

Capping Optimization Depth

The ultimate secret to the DFG JIT's compilation speed is knowing when to stop. Deep, exhaustive optimizations—such as complex loop unrolling or heavy register allocation algorithms—take a significant amount of time to compute.

The DFG intentionally skips these heavy optimizations. Its goal is to generate "good enough" highly optimized code in a matter of milliseconds. If a piece of DFG-compiled code continues to run thousands of times, the engine passes the baton to the final tier, the FTL JIT, which takes on the heavy compilation burden in a background thread while the DFG code continues to run smoothly in the foreground.