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;
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:
Ignition/Sparkplug run; ICs fill the FeedbackVector with Maps, targets, hints.
The tiering manager sees the function is hot and its feedback is stable.
Maglev/TurboFan read the vector and bake the cached shapes into the code — monomorphic loads become direct field loads guarded by a Map check.
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
HeapObject & Map — the shapes ICs key on.
Feedback & tiering decisions — how heat + feedback choose a tier.
TurboFan — the consumer that turns feedback into machine code.