64. Inline caches
Any object can reach any line of code, yet `o.x` is usually just one check and one load. The trick is a tiny memory at each property access, and you can watch it fill up and give up.
- Explain what an inline cache remembers at a property access, and why that makes the access fast.
- Trace an inline cache from uninitialized to monomorphic, polymorphic and megamorphic.
- Read V8’s real --log-ic output for a function that sees five shapes.
- Write code that keeps hot property accesses monomorphic, and build a tiny inline cache yourself.
One line, any object
Here is about the smallest function that reads a property, and the bytecode Ignition makes for it:
// function getX(o) { return o.x; }
// node --print-bytecode
// --print-bytecode-filter=getX
// (addresses and raw bytes removed)
@ 0 : GetNamedProperty a0, [0], [0]
@ 4 : Return
a0 is o. The first [0] is entry 0 of the constant pool, the name
"x". The second [0] is a feedback slot. V8’s source describes this
bytecode in one line: it “calls the LoadIC at FeedBackVector slot
<slot>”[1]. IC stands for inline
cache.
The problem it solves: getX might receive any object at all. Last
chapter you saw that where x lives depends on the object’s shape. Doing
the full search through the shape’s descriptors on every call would be
slow. So each access remembers what it found last time.
Remember what you saw
A postman on a new round looks up every house in the street index. After a week he knows that number 12 is the blue door on the left, and just goes there. He still glances at the number on the door, in case the street has changed. One glance instead of a search.
An inline cache keeps, for one access in the code, pairs of shape and how to get the property for that shape: say, “shape A: load slot 0”. When an object arrives, it checks the object’s shape against what it remembers. If it matches, it does the remembered load. If not, it does the slow lookup and remembers the new answer[2]. Step through one access as five different shapes arrive:
An inline cache (IC) is a small per-access-site memory of the shapes seen there, each with a handler saying how to perform the access for that shape. A hit costs a shape check and the remembered action; a miss falls back to the full lookup and updates the cache. In V8, each IC lives in a feedback slot of the function’s feedback vector[1].
Watching a real cache
Here is the program from the diagram. Five shapes, each with x first,
and ten calls each:
function getX(o) { return o.x; }
const os = [
{ x: 1 }, { x: 1, a: 0 }, { x: 1, b: 0 },
{ x: 1, c: 0 }, { x: 1, d: 0 },
];
for (const o of os) {
for (let i = 0; i < 10; i++) getX(o);
}
V8 has a flag, --log-ic, that logs IC state transitions to a file. (You
may see --trace-ic mentioned online; this Node build refuses it, with
node: bad option: --trace-ic.)
// node --log-ic --logfile=ic.log ic.js
// Node v22.23.4, V8 12.4.254.21
// kind, line:column, old, new, name
LoadIC 1:29 0 1 x
LoadIC 1:29 1 P x
LoadIC 1:29 P P x
LoadIC 1:29 P P x
LoadIC 1:29 P N x
Line 1, column 29 is the x in o.x. Its state went 0 → 1 on the first
shape, 1 → P on the second, stayed P for the third and fourth, and went
P → N on the fifth. The letters come straight from V8’s
source[2].
An IC is monomorphic when only one shape has been seen, polymorphic
when several have, and megamorphic when there have been too many. In
the V8 inside Node 22, “too many” means more than four: the flag
max_valid_polymorphic_map_count defaults to
4[3]. A megamorphic access stops keeping its
own entries and uses the stub cache, a table shared by all megamorphic
loads that maps a shape and a name to a handler[2].
Why the optimizing tiers care
The feedback isn’t only for Ignition. Sparkplug keeps the feedback vector
in its stack frame, because most operations need it[6].
And in 2023 the V8 team described what Maglev does with a monomorphic
o.x: it emits a check that o still has that shape, then a plain load
at a fixed offset. After that it knows the shape, and doesn’t check it
again[5]. A megamorphic site gives it nothing to
specialize on. The idea is old: Hölzle, Chambers and Ungar introduced
polymorphic inline caches for Self in 1991, and noted that the receiver
types a cache records are information an optimizing compiler can
use[9].
The word inline is historical. V8’s old baseline compiler patched the fast path into its machine code. Since Ignition (Chrome 59), the ICs are data-driven: the remembered shapes live in feedback vectors, separate from the bytecode. That separation is also what let V8 cache bytecode after a script has run, as in chapter 62[4].
Keeping sites monomorphic
Megamorphic sites appear in ordinary code. A helper called with many kinds
of objects is one. The V8 team’s 2021 post on super shows another: a
super.m() call inside a mixin, where each use of the mixin creates a new
class, makes that one access site megamorphic[8].
Try it on your device:
function sumA(xs) {
let s = 0;
for (const o of xs) s += o.x;
return s;
}
function sumB(xs) {
let s = 0;
for (const o of xs) s += o.x;
return s;
}
const one = [], many = [];
for (let i = 0; i < 1000; i++) {
one.push({ x: i, k: 0 });
const o = { x: i };
o['k' + (i % 20)] = 0; // 20 shapes
many.push(o);
}
function time(sum, xs) {
const t = performance.now();
for (let r = 0; r < 3000; r++) {
sum(xs);
}
const ms = performance.now() - t;
return ms.toFixed(1) + ' ms';
}
for (let k = 0; k < 2; k++) {
console.log('1 shape: ',
time(sumA, one));
console.log('20 shapes:',
time(sumB, many));
}
// The numbers are your device's.Don’t reshape your whole program around this. A site that sees two or
three shapes is polymorphic, not megamorphic. What matters is the hot code: the inner loop that
reads p.x for every particle. Make every particle with the same
function, in the same order, and that loop sees one shape.
Inline caches and shapes only work together: V8 says ICs don’t work on
dictionary-mode objects at all[7]. This is one
more reason Viscosity’s struct-of-arrays layout helps. In
for (...) xs[i] += vxs[i] there are no named properties left to look up:
each column is a typed array, read by index. The next chapter shows how V8
tracks those accesses, with elements kinds.
Your turn
Objects here are { shape, vals }, made by make(keys, vals), and objects with the same keys share one shape object. slowLookup(o, key) finds a key’s slot the slow way. Write makeIC(key): it returns a function ic(o) that returns the value of key in o. It remembers one shape and its slot. If o.shape is the remembered shape, read o.vals[slot] directly and add 1 to ic.hits. Otherwise call slowLookup, remember the new shape and slot, add 1 to ic.misses, and return the value.
// Simulated objects: { shape, vals }.
const shapes = new Map();
function make(keys, vals) {
const id = keys.join();
if (!shapes.has(id)) {
shapes.set(id, { keys });
}
const shape = shapes.get(id);
return { shape, vals };
}
let lookups = 0;
function slowLookup(o, key) {
lookups++;
return o.shape.keys.indexOf(key);
}
function makeIC(key) {
const ic = (o) => {
ic.misses++;
return o.vals[slowLookup(o, key)];
};
ic.hits = 0;
ic.misses = 0;
return ic;
}Simulate V8’s IC states. icStates(seq) gets the shapes reaching one access site, in order (for example ["A", "A", "B"]). Return the state after each access: "1" if one shape has been seen, "P" for two to four, and "N" once a fifth different shape arrives. Once "N", it stays "N".
function icStates(seq) {
const seen = new Set();
return seq.map((s) => {
seen.add(s);
return seen.size === 1 ? '1' : 'P';
});
}What you learned
- An inline cache remembers, at one property access, the shapes it has seen and how to read the property for each.
- A hit is a shape check plus a load; a miss does the full lookup and updates the cache.
- Monomorphic means one shape, polymorphic a few (up to four in the V8 inside Node 22), megamorphic too many, using a shared stub cache.
- V8’s ICs live in feedback vectors, and Maglev and TurboFan turn monomorphic feedback into a check plus a direct load.
- Keep hot access sites monomorphic by making their objects the same way.
Next: arrays get their own version of shapes, elements kinds.
Check your understanding
0/6- 1.
An inline cache at
o.xis monomorphic for shape A. An object of shape A arrives. What does the access cost? - 2.
Why is it safe for a cache to reuse a slot number once the shape matches?
- 3.
In the
--log-icrun, which line shows the access becoming megamorphic? - 4.
A function is called a million times, always with objects made by one constructor in the same way. Its
p.xis… - 5.
What does a megamorphic access use instead of its own list of shapes?
- 6.
What made V8’s inline caches “data-driven” after Ignition?
References
- [1] The V8 project authors (2024). V8 source code at tag 12.4.254.21: src/interpreter/bytecodes.h and src/interpreter/interpreter-generator.cc. V8 source repository (github.com/v8/v8), tag 12.4.254.21. github.com/v8/v8/blob/12.4.254.21/src/interpreter/interpreter-generator.cc (checked 2026-10-31)
- [2] The V8 project authors (2024). V8 source code at tag 12.4.254.21: src/common/globals.h (InlineCacheState), src/ic/ic.cc and src/ic/stub-cache.h. V8 source repository (github.com/v8/v8), tag 12.4.254.21 (the V8 inside Node v22.23.4). github.com/v8/v8/blob/12.4.254.21/src/ic/ic.cc (checked 2026-10-31)
- [3] The V8 project authors (2024). V8 source code at tag 12.4.254.21: src/flags/flag-definitions.h, src/execution/tiering-manager.cc and src/interpreter/interpreter-assembler.cc. V8 source repository (github.com/v8/v8), tag 12.4.254.21 (the V8 inside Node v22.23.4). github.com/v8/v8/blob/12.4.254.21/src/execution/tiering-manager.cc (checked 2026-10-31)
- [4] Mythri Alle (2018). Improved code caching. V8 blog (v8.dev), 24 April 2018. v8.dev/blog/improved-code-caching (checked 2026-10-31)
- [5] Toon Verwaest, Leszek Swirski, Victor Gomes, Olivier Flückiger, Darius Mercadier, Camillo Bruni (2023). Maglev - V8’s Fastest Optimizing JIT. V8 blog (v8.dev), 5 December 2023. v8.dev/blog/maglev (checked 2026-10-31)
- [6] Leszek Swirski (2021). Sparkplug — a non-optimizing JavaScript compiler. V8 blog (v8.dev), 27 May 2021. v8.dev/blog/sparkplug (checked 2026-10-31)
- [7] Camillo Bruni (2017). Fast properties in V8. V8 blog (v8.dev), 30 August 2017. v8.dev/blog/fast-properties (checked 2026-10-31)
- [8] Marja Hölttä (2021). Super fast super property access. V8 blog (v8.dev), 18 February 2021. v8.dev/blog/fast-super (checked 2026-10-31)
- [9] Urs Hölzle, Craig Chambers, David Ungar (1991). Optimizing Dynamically-Typed Object-Oriented Programming Languages with Polymorphic Inline Caches. ECOOP ’91 Conference Proceedings, Geneva, Switzerland, July 1991; Springer Verlag LNCS 512. bibliography.selflanguage.org/pics.html (checked 2026-11-01)