open atlas
↑ Back to track
JavaScript Engine internals JSE · 03 · 04

Inline caches, deeply: feedback slots and handlers

An IC lives in a feedback-vector slot on the function; a load IC caches {map → handler}. A handler is a Smi-encoded field index for a simple in-object load, or a richer handler object for accessors, prototype, or dictionary loads.

JSE Middle ◷ 15 min
Level
FoundationsJuniorMiddleSenior

The overview in browser/03-v8-internals/04-inline-caches told you an inline cache “records the hidden class and the offset.” True — but where does it record it, and what is the thing it stores next to the map? It is not literally patched into the instruction stream the way the 1990s “inline cache” name suggests. In modern V8 the cache lives in a side table attached to the function, and the value beside the map is a handler — sometimes a single integer, sometimes a small program. This lesson opens that table.

The feedback vector: where ICs actually live

When you see --trace-ic output or a deopt log referencing a “wrong map” or “feedback changed”, you are reading a record of what lives here. Understanding the FeedbackVector is what turns opaque JIT output into a diagnosis.

Each function, once it has run, gets a FeedbackVector — a heap object attached to the function’s closure that holds one or more slots, allocated at compile time, one per IC-bearing operation in the bytecode (each property load, each store, each call, each binary op). The “inline cache” for p.x is not bytes rewritten inside the machine code; it is the contents of that operation’s slot in the FeedbackVector.

This separation matters: the same bytecode can be shared across closures, while the feedback (the observed types and shapes) lives per-FeedbackVector. It is also why the feedback survives tier changes — when Ignition collects feedback and TurboFan later compiles, TurboFan reads exactly these slots. The FeedbackVector is the channel between the interpreter and the optimizing compiler. (We follow that channel forward in 04-the-jit/02-type-feedback.)

A load IC caches {map → handler}

For a property load, a monomorphic IC slot holds two things: the map it expects (the hidden class from lesson 01) and a handler that says how to get the value once the map matches. The fast path is:

load the object's map pointer
compare to the slot's cached map
if equal: apply the handler  -> the value
if not equal: miss -> go to the IC miss runtime, update the slot

The handler is the interesting part. It comes in two broad forms:

  • Smi-encoded handler (the common, fast case). For a plain field load on an object with that map, the handler is just a small integer encoding “load the field at in-object index N” (or “PropertyArray index N”). It is a tagged Smi — no separate allocation. Applying it is a single load at the encoded offset. This is the handler behind the “1 MOV” you saw in the overview.
  • Handler object (the complex case). When the load is not a plain field read, the handler is a LoadHandler heap object describing a small recipe: call this getter (accessor handler), or walk to the prototype and load there (prototype-chain handler, with a validity cell — lesson 05), or do a dictionary lookup (for dictionary-mode objects), or return a constant from the map. These cost more than a Smi handler but are still far cheaper than a fully generic lookup, because the map check already narrowed the case down.
// Conceptually, for a monomorphic load `p.x` where x is an in-object field at index 0:
//   slot = { map: M_point, handler: Smi(encode(in_object, 0)) }
// For `p.size` where size is a getter:
//   slot = { map: M_point, handler: LoadHandler{ kind: accessor, getter: fn } }

Stores have their own ICs — and they cause transitions

The overview focused on loads. Stores (p.x = v) get their own StoreIC slots, and they are more involved because a store can change the shape. A store IC records not just a map and an offset but, when the store adds a property, the transition to take: “from map M1, adding x with these attributes, go to map M2 and write the value at the new field.” So the store IC is where shape transitions from lesson 02 are actually triggered on the hot path, and where representation generalization (Smi → Double) is detected when the stored value does not fit the field’s current representation.

A store IC also distinguishes “store to an existing field” (just write at the offset) from “store that creates a new field” (take the transition, possibly grow the PropertyArray). A store that flips a field’s representation or constness invalidates optimized code that assumed the old one.

BinaryOp and Compare ICs collect number feedback

Not all ICs are about object shape. Arithmetic and comparison operations have BinaryOpIC and CompareIC slots that record the numeric type history of their operands: have we only ever seen Smis here? Smi and HeapNumber (so, Number)? BigInt? String (for +)? Or a mix (Any)?

This feedback is what lets TurboFan emit unboxed integer arithmetic for a loop counter, or a single floating-point instruction for a Number reduction, instead of a general “add anything to anything” runtime call. A loop that has only ever added Smis builds Smi feedback; the day a value overflows to a HeapNumber, the feedback widens and any code specialized on Smi must deopt (the classic overflow deopt from 02-values-and-memory). So the BinaryOp IC is simultaneously accelerating the interpreter and recording the type profile the JIT will trust.

IC and feedback internals (V8)
Where the IC lives
a slot in the function's FeedbackVector
Monomorphic load slot
map + handler
Simple handler
Smi-encoded field index (1 load)
Complex handler
LoadHandler object (accessor/proto/dictionary)
Store IC extra
records the map transition to take
BinaryOp feedback
None/Smi/Number/BigInt/String/Any
Inspect
%DebugPrintFeedbackVector(fn), --trace-ic
Quiz

A monomorphic load IC for `p.x` reads `x` as a getter (accessor property). What does the handler in the slot look like?

Quiz

Why does a StoreIC need to record more than a map and an offset, unlike a simple LoadIC?

Order the steps

Order what happens at a monomorphic load IC for `p.x` when an object of the expected shape arrives.

  1. 1 Read the FeedbackVector slot for this load operation
  2. 2 Load the object's map pointer and compare it to the slot's cached map
  3. 3 On a match, take the slot's handler
  4. 4 Apply the handler — for a plain field, load at the Smi-encoded offset
  5. 5 Return the value without any name comparison or hash lookup
Why this works

Why store the handler separately instead of just an offset? Because “how to fetch a property” is not always “load at offset N.” It might be: invoke a getter, follow the prototype chain to a different object and load there, read a constant folded into the map, or hash-probe a dictionary. Encoding all of those as a uniform “handler” lets the IC stay one shape of slot ({map, handler}) while supporting every kind of access — and lets V8 swap a Smi handler for a richer one without changing the slot’s structure when an access turns out to be more complex than a plain field read.

Recall before you leave
  1. 01
    Where does an inline cache physically live in modern V8, and why does that location matter?
  2. 02
    What is a handler in a load IC, and what are the two main forms?
  3. 03
    How do store ICs and BinaryOp ICs differ from a plain load IC?
Recap

In modern V8 an inline cache is not patched machine code; it is data in the function’s FeedbackVector, a heap object with one slot per IC-bearing operation, allocated at compile time. A monomorphic load slot pairs the expected map (the hidden class) with a handler. The handler is either a Smi-encoded field index — a tagged integer applied as a single load, the fast ‘1 MOV’ path — or a LoadHandler object encoding a recipe for accessor, prototype-chain, dictionary, or constant loads, each still gated by the cheap map check. Store ICs go further: they record the map transition a write takes when it adds a property, so the hot-path store is where transitions and Smi -> Double generalization fire. BinaryOp and Compare ICs record numeric-type feedback (None/Smi/Number/BigInt/String/Any) of their operands. Because all of this lives per-closure in the FeedbackVector and survives tier changes, it is precisely the channel Ignition fills and TurboFan reads — the subject of the JIT unit’s type-feedback lesson. Inspect it with %DebugPrintFeedbackVector and —trace-ic. Now when you see a deopt report mentioning “wrong map” or a --trace-ic line showing a site flip from MONO to POLY, you know exactly what changed in the FeedbackVector slot — and what that means for the JIT tier above it.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 7 done
Connected lessons
appears again in184

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.