V8pedia

Inline caches & feedback

Inline caches (ICs) are how V8 makes operations on a dynamic language fast and how it gathers the data that powers every optimizing tier. An IC is a small per-operation cache that remembers how a site behaved — "this property load saw objects of Map M, and the field was at offset 24" — so it can take a fast path next time. The accumulated observations are the feedback the compilers speculate on.

::: info Ubiquitous language IC: inline cache, attached to a specific operation (load, store, call). Feedback vector: the per-function array where ICs record observations. Nexus: the accessor for one feedback slot. Monomorphic / polymorphic / megamorphic: one shape seen / a few / too many. :::

The problem ICs solve

obj.x in a static language is a fixed offset load. In JavaScript, obj could be any shape, x could be a field, a getter, on the prototype, or absent — and it can differ each time. Resolving that from scratch on every access would be ruinous. But in practice a given source site almost always sees the same shape repeatedly. ICs exploit that: pay full price once, cache the result keyed by Map, and fast-path every subsequent matching access.

The state machine

An IC progresses through states as it sees more shapes:

enum class InlineCacheState {
  NO_FEEDBACK,
  UNINITIALIZED,      // never run
  MONOMORPHIC,        // exactly one Map seen
  RECOMPUTE_HANDLER,
  POLYMORPHIC,        // a few Maps seen
  MEGADOM, HOMOMORPHIC,
  MEGAMORPHIC,        // too many — give up tracking individuals
  GENERIC,
};

src/common/globals.h#L1861-L1880

The progression is monotonic and each step is slower than the last:

  • Monomorphic — one Map. The fast path is a single Map compare + a direct field load. This is the case the optimizers love.

  • Polymorphic — a handful of Maps (up to a small limit). The IC stores an array of (Map → handler) pairs and linear-searches it. Still far faster than a full lookup.

  • Megamorphic — too many Maps to track. The IC stops recording individuals and defers to a shared global hash table, the stub cache.

This lattice is the runtime embodiment of the shape-discipline rule: keep sites monomorphic and everything downstream is fast.

Feedback vectors: where it's stored

Every function with feedback has a FeedbackVector — observations for all its IC sites, plus the function's heat counters:

V8_OBJECT class FeedbackVector : public HeapObject {
  inline int32_t invocation_count() const;
  inline uint8_t osr_state() const;
  FeedbackSlotKind GetKind(FeedbackSlot slot) const;
  // …a flexible tail of feedback slots…
  FLEXIBLE_ARRAY_MEMBER(TaggedMember<MaybeObject>, raw_feedback_slots);
};

src/objects/feedback-vector.h#L308-L556

The slot layout is fixed at compile time by FeedbackMetadata (a bit-packed table of each slot's FeedbackSlotKind — load, store, call, binary-op, compare, for-in, …), allocated when the BytecodeGenerator assigns slots. Different kinds use different slot counts (a property IC uses two slots — a Map and a handler — while a binary-op uses one bit-packed slot).

Reading a slot: the nexus

Code reads and writes a slot through a FeedbackNexus, which also derives the current IC state from the slot's contents:

InlineCacheState FeedbackNexus::ic_state() const {

  if (feedback == UninitializedSentinel()) return UNINITIALIZED;
  if (feedback == MegamorphicSentinel())   return MEGAMORPHIC;
  if (feedback.IsWeakOrCleared())          return MONOMORPHIC;   // a single (weak) Map
  if (IsWeakFixedArray(heap_object))       return POLYMORPHIC;   // array of Maps

}

src/objects/feedback-vector.cc#L686-L758

Note the weak reference to the Map: an IC must not keep an object's shape alive on its own, or caches would leak memory and prevent Maps from dying. If the Map is collected, the slot clears and the IC falls back gracefully.

The transition logic

IC::SetCache drives the state machine as new shapes arrive — monomorphic → polymorphic → (if the polymorphic limit is exceeded) megamorphic:

case MONOMORPHIC:

case POLYMORPHIC:
  if (UpdatePolymorphicIC(name, handler)) break;

case MEGAMORPHIC:
  UpdateMegamorphicCache(lookup_start_object_map(), name, handler);
  break;

src/ic/ic.cc#L984-L1042

The stub cache

Megamorphic sites share a process-wide two-level hash table — the stub cache — keyed by (Map, name):

static const int kPrimaryTableBits  = 12;  // 4096 entries
static const int kSecondaryTableBits = 10; // 1024 entries

src/ic/stub-cache.h#L15-L145

A megamorphic load hashes (Map, name), probes the primary then secondary table, and runs the handler if found. Slower than monomorphic, but still far cheaper than a full property lookup — it keeps even hopelessly-polymorphic code reasonable.

More than shapes: call and operator feedback

ICs also record call targets (with a call count, used to decide inlining) and operator type hints for binary/compare ops:

BinaryOperationHint  GetBinaryOperationFeedback()  const;
CompareOperationHint GetCompareOperationFeedback() const;
int                  GetCallCount();

src/objects/feedback-vector.h#L1047-L1076

A + that has only seen Smis records kSignedSmall; the optimizers then emit integer addition with an overflow guard instead of a generic, polymorphic Add.

How feedback becomes speed

The loop closes here:

  1. Ignition/Sparkplug run; ICs fill the FeedbackVector with Maps, targets, hints.

  2. The tiering manager sees the function is hot and its feedback is stable.

  3. Maglev/TurboFan read the vector and bake the cached shapes into the code — monomorphic loads become direct field loads guarded by a Map check.

  4. If the guard ever fails, deopt restores the interpreter.

Monomorphic feedback is the ideal input; megamorphic feedback gives the optimizers nothing to specialize on. This is the mechanical reason "keep your object shapes consistent" is the cardinal V8 performance rule.

See also