Speculation and Type Feedback

Two programs, the same function, the same amount of arithmetic on paper. One finishes in a blink. The other crawls, and staring at the code will not tell you why, because the slow one is not doing anything extra.

function norm(p) {
  return Math.sqrt(p.x * p.x + p.y * p.y);
}

Feed norm a million points that were all built the same way, { x, y }, and V8 turns that body into a short run of machine instructions. Feed it a million points built five different ways, some { x, y }, some { y, x }, some with a z bolted on, some that got their x assigned later, and the same function runs measurably slower while doing the identical multiplications and the identical square root.

The math never changed. What changed is how much the engine was able to remember about p.

That memory is what this lesson is about. Optimised code is fast because it specialises for specific types, and the engine only knows those types because it watched your code run and wrote down what it saw. Before any tier can speculate, something has to gather the evidence. This is where the evidence lives.

The engine keeps notes on your function

When the interpreter runs your bytecode, it is not just executing. It is also taking notes. Every operation that could behave differently depending on the types involved, a property read, an arithmetic operator, a function call, has a small scratchpad slot attached to it, and each time that operation runs it scribbles down what it actually encountered.

The interpreter lesson showed you the [0] sitting on an Add instruction in a bytecode dump. That number is an index into a side table called the feedback vector, one per function, and the note the Add writes goes into slot [0]. A property load writes into its own slot. A call writes into another. Over many runs those slots fill up with a profile of reality: this add only ever saw integers, that property load always saw one kind of object, this call always reached the same function.

the same load p.x, recorded across three runsrun 1p has shape Arun 2p has shape Arun 3p has shape Afeedback slot for load p.xshape A, seen every timemonomorphicthe slot is a profile: what this one operation actually saw
The same property load, run three times. Each run records the shape it saw into the function's feedback slot, building up a profile of what actually happened.

None of this is something you can read from JavaScript. The feedback vector is an internal structure the engine owns. But it is the raw material every optimising tier depends on, so when TurboFan or Maglev decide to specialise norm, this is the evidence they read. No feedback, nothing to bet on.

Three words that run the rest of this part

Here is the vocabulary that shows up in every lesson after this one, and it is worth burning in. It describes how much variety a single operation has seen.

An operation is monomorphic when it has only ever seen one shape or type. This is the best case. One possibility means the engine can specialise hard: check that the input still matches the one thing it expects, then do the fast, direct operation. This is where the single-instruction add and the direct field load come from.

It is polymorphic when it has seen a small handful of different shapes. The engine can still cope. Instead of one check it emits a short chain, is it shape A, or B, or C, and handles each. Slower than monomorphic because there is more checking, but still specialised and still fast.

It is megamorphic when it has seen so many shapes that the engine gives up trying to track them. At that point specialising is not worth it, so the operation falls back to a fully generic path: a general lookup that works for anything and is optimised for nothing.

more distinct shapes seen, left to rightspecialisation drops, cost risesmonomorphic1 shapeAone shape check,then a direct loadfastpolymorphica few shapes (up to ~4 in V8)ABCa short chain of checks,still specialisedokmegamorphictoo many shapesand more, untrackedengine stops tracking,generic slow lookupslowthe fall that hurts is polymorphic to megamorphic: specialisation is abandoned there
One operation, three fates. As it sees more distinct shapes it moves from a single check, to a short chain, to giving up and running a generic lookup. Speed drops at each step.

Say them out loud once: mono, poly, mega. One type, a few types, too many. Consistency sits on the left and is fast. Variety slides you rightward, and past a point the engine stops rewarding you for it. That single sentence is the most actionable idea in this whole part, and everything practical below is a consequence of it.

Why monomorphic wins: the feedback becomes a guard

The speed difference is not magic, and it is worth seeing exactly where it comes from. Take a monomorphic property load. Its feedback slot says “every object here had shape A, and the property lived at offset 2”. When an optimising tier compiles this, it does not emit a general property lookup. It turns that recorded fact into a single cheap check: is this object’s shape still A? If yes, read the value straight from offset 2, one instruction, done. This is the inline cache idea, and the check has a name that echoes through the optimising-compiler lesson: a guard.

The guard is the whole trick. Speculation means betting the future looks like the past, and a bet you might lose has to be checked. So the recorded feedback becomes a guard that gates the fast path. Pass the guard, run the specialised code. Fail it, and the fast code cannot handle this new case, so the engine throws it out and drops back to the interpreter, which handles everything. That fallback is deoptimisation, and it has its own lesson. The part that matters here is that a wrong guess is only ever slower, never wrong.

the recorded shape becomes a cheap guardfeedback: this load only ever saw shape Aa record of the past, not a promiseguardis p still shape A?holdschangeddirect load at a fixed offsetone instruction, no name lookupfast, while the shape holdsdeoptimisediscard the fast coderesume in the interpretermonomorphic is one check; polymorphic a short chain; megamorphic is not worth guarding
A monomorphic feedback record compiles into one cheap guard. While the shape holds, the fast direct load runs; when it changes, the code deoptimises back to the interpreter.

Now the cost ladder makes sense. Monomorphic is one guard: cheap, and the CPU’s branch predictor learns instantly that it always passes. Polymorphic is a small chain of guards, still cheap but more of them. Megamorphic has too many possibilities to guard usefully, so there is no fast path to gate, just the generic lookup for everyone. The guards are the reason consistency pays, and they are also why a broken assumption is safe: the check catches the divergence before the wrong code can run.

Not every operation counts shapes

The mono, poly, mega ladder is easiest to picture with property access, where the thing being counted is object shapes. But the feedback vector watches other operations too, and each one records the kind of thing that matters for it. Lumping them all together as “shapes” is a common misunderstanding, so it is worth pulling apart.

property access
p.x
records which object shapes it saw, and counts them: one, a few, or too many
arithmetic
a + b
records a type and widens it: small integer, then number, then anything. Narrower is better
function call
fn(x)
records which function it reached, so a steady target can be inlined into the caller
Different operations record different feedback. Property access counts object shapes; arithmetic widens a type from narrow to general; a call remembers which function it reached.

For arithmetic there is no shape to count. The + in norm records a type, and that type generalises up a ladder rather than filling a set of slots. See only small integers and the feedback stays as narrow as it gets, which is what lets the add become a single machine instruction. Mix in fractional numbers and it widens to “number”, still good. Throw a string or an object through the same operator and it widens toward “anything”, at which point the operator is back to asking every question at runtime. Same direction as the shape ladder, narrow and specific is fast, broad and general is slow, but the mechanism is a widening type rather than a count.

For a call, the feedback is simply which function ran. A call site that always reaches the same function can have that function pasted into the caller, which is the single biggest optimisation the top tier has. A call site that reaches a different function every time cannot, for the same reason a megamorphic load cannot specialise: too much variety to bet on.

Monomorphic code and megamorphic code, side by side

Enough theory. Here are two functions doing the same job, and the difference between them is entirely in what you feed them.

The first stays monomorphic because every object handed to it was built the same way, so they all share one shape:

function makeUser(id, name) {
  return { id, name };
}

function idOf(u) {
  return u.id; // this load only ever sees one shape
}

const users = [];
for (let i = 0; i < 100000; i++) {
  users.push(makeUser(i, "u" + i));
}
// every u has the same shape → idOf's load stays monomorphic
users.forEach(idOf);

Every object came out of the same factory with the same properties in the same order, so the load inside idOf sees exactly one shape forever. Monomorphic, one guard, direct load. This is the fast path, and it is also just normal, readable code. You did not do anything clever to get it. You were simply consistent.

The second does the identical .id read, but its objects were assembled every which way:

function idOf(u) {
  return u.id; // same code, but now fed a zoo of shapes
}

const mixed = [
  { id: 1, name: "a" },
  { name: "b", id: 2 },        // properties in a different order → different shape
  { id: 3, role: "admin" },    // extra property → different shape
  { id: 4 },                   // missing property → different shape
  makeLegacyUser(5),           // built elsewhere → different shape again
];
// idOf's load sees 5+ shapes → it goes megamorphic
mixed.forEach(idOf);

Property order matters. Missing or extra properties matter. An object built by a different function matters. Each of those is a distinct shape, and once idOf’s load has seen more than a handful, it goes megamorphic and drops to the generic lookup for every call, including the ones that were shape A all along. The code is character-for-character the same. The inputs poisoned it.

same idOf(u), the only difference is the variety of uone caller, one shapeAAAidOf(u) return u.id1 shape, monomorphiccompiles to a direct loadfasta different shape each timeABCDEidOf(u) return u.id5+ shapes, megamorphicstuck on the generic lookupslow
Identical function, opposite outcomes. Fed one shape it stays monomorphic and compiles to a direct load; fed many shapes it goes megamorphic and is stuck on the generic lookup.

Try it directly below. You are the caller. Each button feeds idOf one more object of a shape you pick, and the feedback panel tracks the distinct shapes it has seen, labels the site monomorphic, polymorphic or megamorphic, and drops the speed meter as the variety climbs. Everything here is a simulation of how the feedback state moves, with canned numbers, not a live measurement of V8:

interactiveFeed a call site shapes and watch its feedback state

Notice the cruel part of megamorphic: it does not recover on its own just because you go back to sending shape A. Once the site gave up, it stays given up. That is why one careless caller, somewhere, feeding an odd shape into an otherwise clean hot path can quietly tax every other caller.

Polymorphism is fine, actually

It would be easy to read all this as “monomorphic good, everything else bad” and start contorting your code to chase a single shape everywhere. Do not. That is how internals knowledge turns into superstition.

Polymorphism is normal and usually harmless. A function that handles two or three related shapes is doing exactly what the polymorphic path was built for, and the short chain of checks is cheap. Plenty of perfectly fast code is mildly polymorphic and always will be. The engine designed for this on purpose.

Megamorphic is not automatically a disaster either. Think about a generic helper like a logger, a deep-clone, or a serialiser. It touches every kind of object in your app by design, so its property loads are megamorphic and there is nothing to fix, because it is generic on purpose. That only becomes a problem if such a site sits in a genuinely hot loop, and even then the answer is to measure before you rearrange anything.

Same idea in every engine

Feedback vectors and inline caches are V8’s implementation, but the concept is not V8’s. Every serious engine has to solve the same problem, watch a dynamically typed program run, learn what types actually show up, and specialise for them, so every one of them has a feedback structure and the same monomorphic-to-megamorphic distinction.

What this means for your code

You cannot touch the feedback vector, and you should not try to manage it. What you control is how easy you make it to keep the notes consistent, and it comes down to being predictable.

  • Build objects the same way every time. Same properties, same order, ideally from the same constructor or factory, so they share one shape and the loads that read them stay monomorphic. Adding properties in different orders, or bolting them on later, mints new shapes and pushes toward megamorphic.
  • Feed hot functions steady types. A function called with numbers in one place and strings in another gives its arithmetic and its loads nothing consistent to specialise on. One steady type per site is what earns the single-instruction path.
  • Know which sites are generic on purpose. A serialiser or logger is megamorphic by nature, and that is fine. The thing to catch is a genuinely hot loop that went megamorphic by accident, because one caller feeds it a shape the others never do.
  • Measure before you rearrange. This model is for reading a profiler, not for pre-emptively twisting your code. The everyday advice, small predictable functions and consistently built objects, gives you good feedback for free. Benchmarks can hide all of this if you time the wrong phase, so warm the code up and use realistic inputs.

Summary

  • As the interpreter runs, it records what each type-sensitive operation actually saw into a per-function feedback vector. This is the memory that speculation is built on.
  • The feedback classifies each operation as monomorphic (one shape or type, the fast case), polymorphic (a few, still specialised), or megamorphic (too many, the engine gives up and runs a generic path). Consistency is fast; variety is slow.
  • The recorded feedback compiles into cheap guards that gate the specialised code. A monomorphic site needs one check; a polymorphic site a short chain; a megamorphic site has nothing worth guarding. A failed guard deoptimises rather than computing a wrong answer.
  • Different operations record different things: property access counts object shapes, arithmetic widens a type from narrow to general, and a call remembers which function it reached (so it can be inlined). The mono, poly, mega direction is the same for all of them.
  • The “up to four shapes” figure and the tier names are V8-specific and can change. Every engine keeps per-operation feedback and has the same three states under different names.
  • Polymorphism is normal and megamorphism is not always a bug. This is a diagnostic model for reading a profiler, not a licence to litter code with type tricks.

What this means for your code: you never fill the feedback vector by hand, but you decide what it records. Build your objects consistently and pass your hot functions steady types, and the notes stay clean enough for the optimiser to bet on. Send a site a zoo of shapes and it stops betting on any of them.