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

Monomorphic, polymorphic, megamorphic — and the stub cache

The IC state machine: uninitialized → premonomorphic → monomorphic (1 map) → polymorphic (2-4 maps, a scanned list) → megamorphic (≥5 maps, consult the global megamorphic stub cache, a hash probe far slower and JIT-hostile). Prototype ICs use validity cells.

JSE Middle ◷ 15 min
Level
FoundationsJuniorMiddleSenior

The overview in browser/03-v8-internals/04-inline-caches gave you the four-word version: mono → poly → mega, and “megamorphic is slow.” But when an IC goes megamorphic, where does the lookup actually go? It does not just shrug and do a name search every time — V8 has a global, process-wide hash table, the megamorphic stub cache, that every megamorphic site on the planet shares. This lesson walks the full state machine and opens that shared cache, plus the validity cells that police prototype-chain ICs.

The full state machine, not just three words

A load IC slot transitions through more states than the overview implied. The full ladder, one-way toward megamorphic:

  • uninitialized — the slot has never run.
  • premonomorphic — V8’s “seen once, waiting to confirm” state. The very first execution often records the map here without yet emitting a specialized handler, so a one-shot path does not pay for an IC it will never reuse. The second hit with the same map promotes to monomorphic.
  • monomorphic — exactly one map. The slot is {map, handler} (lesson 04). On a hit: load map, compare, apply handler — a couple of instructions. This is the target state.
  • polymorphic2 to 4 maps. The slot becomes a small list of {map, handler} pairs. A load scans the list (a few compares) and applies the matching handler. Still fast, but the branch chain grows with each shape.
  • megamorphic5 or more maps. V8 stops tracking maps at this site entirely. The slot is marked megamorphic, and lookups fall through to the megamorphic stub cache.

The megamorphic stub cache: a process-wide hash table

This is the part the overview never mentions. When a site is megamorphic, V8 does not do a fresh property search on every access. Instead it consults a single, global (per-isolate) hash table — the megamorphic stub cache — keyed by (map, property name) and storing the handler. The access becomes:

  1. Compute a hash from the object’s map and the property name.
  2. Probe the stub cache (it is a small, fixed-size open-addressed table — primary and secondary tables).
  3. On a hit, use the cached handler. On a miss, do the full runtime lookup and insert the result.

So a megamorphic site is a hash probe plus a likely handler application — much slower than a monomorphic compare-and-load (tens to low hundreds of cycles vs ~1), and because the table is shared and fixed-size, different megamorphic sites evict each other’s entries. The cost is not just the probe; it is that the site can no longer be specialized.

Why megamorphic is JIT-hostile, not just slower

The deeper cost is what it does to TurboFan. A monomorphic or low-polymorphic site gives the optimizer a small, known set of shapes, so it can inline the field load (or the whole method) and guard with a cheap map check. A megamorphic site gives TurboFan no usable shape information — the feedback is “anything” — so TurboFan must emit a generic call to the IC stub-cache machinery and cannot inline the access or anything reachable through it. One megamorphic property access in a hot inlined method can therefore block a whole chain of optimizations, costing far more than the per-access hash probe alone.

Prototype-chain ICs and validity cells

Many real loads do not find the property on the object itself but on its prototype (methods live on Class.prototype). A prototype-chain IC caches “the property is N levels up the chain, at this offset” — but that is only safe while the prototype chain has not changed. V8 guards it with a validity cell: a small heap cell, shared by the maps that depend on a given prototype, that is “valid” until someone mutates that prototype (adds/removes a property on it, or reassigns it). A prototype IC’s fast path is: check map, check the validity cell is still valid, then load.

The teeth: mutating a prototype after objects exist invalidates the validity cell, which deoptimizes every IC and every TurboFan-compiled function that relied on it. This is why monkey-patching Array.prototype or assigning to SomeClass.prototype.method at runtime (after the hot paths warmed up) can cause a sudden, global-feeling slowdown — you tripped a validity cell that thousands of sites trusted.

IC states and the stub cache (V8)
Monomorphic load
~1 cycle (compare + load)
Polymorphic
2-4 maps, scan a short list
Megamorphic threshold
5 distinct maps at the site
Megamorphic lookup
global stub-cache hash probe (tens-hundreds of cycles)
Stub cache scope
per-isolate, fixed-size, shared by all mega sites
Prototype IC guard
validity cell (invalidated on proto mutation)
Inspect
--trace-ic prints MONO/POLY/MEGA per site
Quiz

A property-access site has gone megamorphic. What does an access do now, mechanically?

Quiz

An app warms up, then a library does `Array.prototype.last = function(){...}` at runtime. Hot array code suddenly slows across the board. Why?

Order the steps

Order the IC states a load site passes through as it observes 1, then 2, then 5 distinct maps.

  1. 1 uninitialized — never executed
  2. 2 premonomorphic — first map seen, waiting to confirm reuse
  3. 3 monomorphic — one map, {map, handler}
  4. 4 polymorphic — 2 to 4 maps, a scanned list of pairs
  5. 5 megamorphic — 5+ maps, falls through to the global stub cache
Why this works

Why does a global stub cache help at all, instead of just doing a runtime lookup at every megamorphic site? Because even though a single site sees many shapes, the combinations of (map, name) that the whole program touches are far fewer than the number of accesses. Caching them in one shared table means the second time any megamorphic site loads property x off map M, it gets the handler from the cache rather than re-deriving it. It is a damage-control structure: it cannot restore inlining or monomorphic speed, but it keeps a megamorphic site from being a full lookup every single time.

Recall before you leave
  1. 01
    Walk the full IC state machine, including the state the overview omitted, and the map count for each.
  2. 02
    What is the megamorphic stub cache and why is going megamorphic worse than just 'a few more compares'?
  3. 03
    What is a validity cell and how does mutating a prototype cause a broad slowdown?
Recap

An inline cache climbs a one-way ladder: uninitialized → premonomorphic (the state the overview skipped, where V8 records a first map but waits to confirm reuse before specializing) → monomorphic (exactly one map, a {map, handler} slot read in ~1 cycle) → polymorphic (2 to 4 maps, a short scanned list of pairs) → megamorphic (5 or more maps). At megamorphic, V8 abandons per-site map tracking and routes lookups through the megamorphic stub cache: a single per-isolate, fixed-size hash table keyed by (map, name), shared by all megamorphic sites, so accesses become hash probes and unrelated sites evict each other. The deeper penalty is that megamorphic feedback carries no shape, so TurboFan cannot inline or specialize the access — one such load can block a chain of optimizations. Prototype-chain ICs are guarded by validity cells; mutating a prototype after warm-up invalidates the shared cell and deoptimizes every dependent IC and compiled function at once. Because the states are one-way, you cannot un-megamorphic a live site — you fix it upstream by keeping shapes stable and construction consistent, so each hot site sees one map. This feeds directly into TurboFan’s type-feedback consumption in unit 04. Now when you see a function that refuses to hit top tier or keep bouncing between tiers, your first stop is --trace-ic: find the site that flipped MEGA, trace the shapes back to where they diverge, and fix the construction order there.

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 5 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.

Apply this

Put this lesson to work on a real build.

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.