V8pedia

Deoptimization

Speculative optimization (Maglev, TurboFan) is only sound because V8 can undo it. Deoptimization is the act of bailing out of optimized code and resuming in a lower tier when a speculated assumption turns out to be false. It is the safety net that lets the optimizers bet aggressively.

::: info Ubiquitous language Deopt: leaving optimized code and continuing in the interpreter (or baseline). Eager deopt: triggered now, by a guard inside running optimized code. Lazy deopt: the code is marked invalid and bails out the next time it is entered. Bailout: a deopt with a recorded reason. :::

Why it must exist

TurboFan might compile function add(a, b) { return a + b; } assuming a and b are always Smis, because that is all the feedback ever saw. That assumption can break at any moment — someone calls add("x", "y"). The optimized code cannot handle strings; it must hand control back to code that can. Without a correct deopt mechanism, speculation would be a correctness bug, not an optimization.

The hard part: reconstructing the frame

The optimized frame and the interpreter frame are completely different. Optimized code keeps values in registers and untagged forms, inlines callees, and omits state the interpreter needs. To resume in Ignition, the deoptimizer must rebuild the interpreter frame(s) — including any frames that were inlined away — from the optimized state, using metadata the compiler recorded at each possible deopt point.

class Deoptimizer : public Malloced {
 public:
  static Deoptimizer* New(Address raw_function, DeoptimizeKind kind,
                          Address from, int fp_to_sp_delta, Isolate*);
  static void DeoptimizeFunction(Tagged<JSFunction>, LazyDeoptimizeReason,
                                 Tagged<Code> = {});
  DeoptimizeKind deopt_kind() const;
  BytecodeOffset bytecode_offset_in_outermost_frame() const;
};

src/deoptimizer/deoptimizer.h#L36-L150

The rebuilt frame is assembled in a FrameDescription — a buffer holding the reconstructed registers, PC, and stack slots, plus a continuation PC pointing at the bytecode where execution resumes:

class FrameDescription {
  void SetPc(intptr_t pc);
  void SetContinuation(intptr_t pc);   // where the interpreter picks up
  intptr_t GetFrameSlot(unsigned offset);
  void SetFrameSlot(unsigned offset, intptr_t value);
  // …register values + variable-size frame content…
};

src/deoptimizer/frame-description.h#L67-L282

::: details Where does the reconstruction data come from? At every point where it might deopt, the optimizing compiler records a "translation": a description of how to recover each interpreter register/stack-slot from the optimized state (a machine register, a stack slot, a constant, or a re-materialized object). The deoptimizer replays these translations to rebuild the frame(s). Inlined functions expand into multiple reconstructed frames. This metadata is part of why optimized code is bigger than baseline code. :::

Eager vs lazy

  • Eager deopt happens inside running optimized code: a guard fails (the Map check, the Smi check, the array-not-holey check), and control jumps immediately to the deopt path. Recall that TurboFan's schedule even has a kDeoptimize block terminator.

  • Lazy deopt happens when code becomes invalid while not currently running — e.g. a code dependency is invalidated because a prototype changed. The code is marked; the next entry redirects to deopt rather than executing the stale optimized body. This keeps invalidation off the hot path.

Reasons, and learning from them

Every deopt carries a reason, from a long enumerated list:

V(ArrayBufferWasDetached, "array buffer was detached or immutable")
V(ArrayLengthChanged,     "the array length changed")
V(Smi,                    "Smi")
V(PrepareForOnStackReplacement, "prepare for on stack replacement (OSR)")

src/deoptimizer/deoptimize-reason.h

Reasons are gold for performance work: --trace-deopt in d8 shows exactly which assumption broke and where. V8 also remembers deopts — a function that "was once deoptimized" (a flag on its FeedbackVector) is tiered up more cautiously, and repeated deopts can disable optimization for that function entirely. A program that deoptimizes in a loop is pathological: it pays to compile and never gets to keep the fast code.

::: warning The deopt loop anti-pattern If optimized code deopts, runs in the interpreter, gets hot, re-optimizes on the same bad assumption, and deopts again, you have a deopt loop — the worst of all worlds. The usual cause is genuinely polymorphic types at a hot site. The cure is shape stability (Maps), not fighting the compiler. :::

See also