Dart 92 🎯 Internal Architecture: Inline Caches (IC) and Polymorphic Call Sites
Inline caches are the Dart VM’s mechanism for making dynamic method dispatch fast without giving up the flexibility of a dynamically typed language. The first time a call site executes, the VM does not know which class the receiver will be. The call goes through a resolution stub, the target is looked up, and the result is cached at the call site. On subsequent executions, the cached class check is fast: compare the receiver’s class ID against the cached value, and if it matches, jump directly to the cached target. The complexity appears when a call site sees more than one receiver class—a polymorphic call site—and the VM must decide whether to keep collecting type feedback, switch to a hash-table lookup, or let the optimizing compiler specialize the call.
Key point: A call site’s IC state determines how dispatch works. Monomorphic call sites check one class and jump. Polymorphic call sites scan a small linear list of class-target pairs. Megamorphic call sites exceed the polymorphic threshold and switch to a hash table, which is slower per call but prevents unbounded type feedback collection .
Why inline caches exist
The dynamic dispatch problem. In a language where the receiver’s class is not known at compile time, every method call requires a lookup. The VM must ask: what class is this object, and which implementation of the method does that class provide? A naive implementation would perform this lookup on every call. An inline cache exists to cache the answer at the call site. Once a call site has seen a receiver class, it can check for that class with a single integer comparison and jump directly to the cached target .
The type feedback problem. The unoptimized code collects type feedback at call sites. When the optimizing compiler later compiles a function, it uses this feedback to specialize the generated code. A call site that has only ever seen one class can be compiled as a direct call with a class check. A call site that has seen several classes can be compiled with polymorphic inlining, where each class’s implementation is inlined behind its own guard . Without type feedback, the optimizer would have no basis for specialization.
The unbounded collection problem. If a call site sees hundreds of different receiver classes, collecting a linear list of every class-target pair becomes a memory and scanning cost that grows without bound. The VM switches such call sites to a megamorphic cache, which uses a hash table keyed by class ID. This prevents the IC data from growing indefinitely and gives a bounded, if slower, lookup path . The trade-off is a loss of precise target frequency information, which the optimizer uses to order polymorphic inlining decisions .
a. IC States and the Call Site Lifecycle
A call site in unoptimized Dart code begins in an unlinked state. The first execution triggers a resolution path that determines the target function for the receiver’s class. The call site is then patched to a monomorphic stub that checks the expected class ID and jumps to the target .
If the call site executes again with the same receiver class, the monomorphic check succeeds and the dispatch is fast. If a different class appears, the monomorphic check fails, and the miss handler adds the new class-target pair to the IC data. The call site becomes polymorphic. The stub now performs a linear scan against the IC data array, which stores groups of class IDs and their corresponding target functions .
// ICData array format (conceptual)
// The array stores checks, each containing:
// - N classes (receiver classes checked)
// - 1 target function (where to jump if the classes match)
// Example: polymorphic call site with three receiver classes
┌─────────────────────────────────────────────────────────┐
│ Check 1: [ClassA] → FunctionA │
│ Check 2: [ClassB] → FunctionB │
│ Check 3: [ClassC] → FunctionC │
│ Terminator: [null] → null │
└─────────────────────────────────────────────────────────┘
The linear scan is efficient for a small number of classes. The VM’s threshold for switching to megamorphic is controlled by FLAG_max_polymorphic_checks. When the number of checks exceeds this threshold, the call site is switched to a megamorphic cache . The megamorphic stub hashes the receiver’s class ID and looks up the target in a hash table. This is slower than a monomorphic check but avoids the linear scan cost and the memory cost of storing an ever-growing list .
b. Monomorphic and Megamorphic Specialization
The unoptimized code’s IC state is one thing; the optimized code’s dispatch is another. When the JIT compiler optimizes a function containing a call site, it reads the IC data and generates specialized code. For a monomorphic call site, the optimizer generates a prologue that checks the expected receiver class and jumps to the target. In JIT mode, there is additional indirection to tick a usage counter, which helps the inliner make decisions .
For a megamorphic call site, the optimizer cannot specialize on a small set of classes. Instead, it generates a stub that performs a hash table lookup against a MegamorphicCache . The generated code is more complex than the monomorphic check but still faster than a full dynamic lookup. The megamorphic path does not record the call site’s usage count; it approximates usage using the target function’s counter .
Hot reload resets monomorphic and megamorphic call sites back to the polymorphic or unlinked state. This is necessary because the class hierarchy or method implementations may have changed, and the cached targets may no longer be valid . The same reset happens when the debugger begins stepping, because the specialized stubs do not check the stepping state.
c. The Polymorphic Dispatch Mechanism
A polymorphic call site’s stub checks the receiver’s class against each entry in the IC data. The array format is specific: it stores the number of arguments checked, then groups of class IDs followed by a target function . If the receiver’s class matches a group, the stub jumps to the associated target. If no group matches, an IC miss occurs, and the miss handler adds the new class-target pair to the array.
The number of classes that can be checked in a polymorphic stub is limited. The VM uses FLAG_max_polymorphic_checks to decide when to switch to megamorphic. The exact value is an implementation detail, but the principle is that the linear scan must stay cheap. Beyond a small number of classes, the scan cost approaches the hash lookup cost, and the memory cost of the IC data grows .
The optimizer uses the polymorphic IC data to perform polymorphic inlining. If a call site has seen three classes, the optimizer can inline all three implementations, each behind its own class check. The order of the checks matters for performance; the optimizer uses the frequency information in the IC data to put the most common class first . Megamorphic call sites lose this frequency information, which is why the switch to megamorphic is a trade-off: it bounds memory and scan cost but sacrifices optimization opportunities .
Complete Example Session
// ============================================
// PART 1: A DYNAMIC CALL SITE
// ============================================
// The call site is the same for all receiver classes
// The VM must determine the target at runtime
void speak(Animal animal) {
animal.sound(); // ← call site
}
class Animal {
void sound() => print('...');
}
class Dog extends Animal {
@override
void sound() => print('Woof');
}
class Cat extends Animal {
@override
void sound() => print('Meow');
}
// ============================================
// PART 2: FIRST CALL — UNLINKED TO MONOMORPHIC
// ============================================
void firstCall() {
speak(Dog()); // Unlinked → IC miss → resolves Dog.sound
// Call site patched to monomorphic stub
// Stub checks: receiver is Dog? → jump to Dog.sound
}
// ============================================
// PART 3: SECOND CALL — SAME CLASS
// ============================================
void secondCall() {
speak(Dog()); // Monomorphic check succeeds
// Class ID matches, direct jump
// No lookup, no IC data scan
}
// ============================================
// PART 4: DIFFERENT CLASS — POLYMORPHIC
// ============================================
void polymorphic() {
speak(Cat()); // Monomorphic check fails
// IC miss handler adds Cat → Cat.sound
// Call site becomes polymorphic
// Stub now scans: [Dog → Dog.sound, Cat → Cat.sound]
}
// ============================================
// PART 5: MORE CLASSES — POLYMORPHIC SCAN
// ============================================
void moreClasses() {
speak(Dog()); // Scan: Dog? yes → Dog.sound
speak(Cat()); // Scan: Dog? no. Cat? yes → Cat.sound
speak(Animal()); // Scan: Dog? no. Cat? no. → IC miss
// Adds Animal → Animal.sound
}
// ============================================
// PART 6: EXCEEDING POLYMORPHIC THRESHOLD
// ============================================
// After FLAG_max_polymorphic_checks classes
// the call site switches to megamorphic
void megamorphic() {
// The stub now uses a hash table
// receiver class ID → hash → bucket lookup → target
// No linear scan, bounded memory
}
// ============================================
// PART 7: OPTIMIZER READS IC DATA
// ============================================
// The JIT compiler compiles speak() with
// knowledge of the call site's type feedback
void optimizedCode() {
// If monomorphic: direct call with class check
// If polymorphic: inline each target behind its own check
// If megamorphic: hash table lookup stub
}
// ============================================
// PART 8: HOT RELOAD RESETS IC
// ============================================
// When hot reload occurs:
// Monomorphic and megamorphic call sites
// are reset to polymorphic/unlinked state
// Cached targets may be invalid after code changes
// ============================================
// PART 9: IC DATA ARRAY STRUCTURE
// ============================================
// Conceptual representation of ICData
// [num_args_checked,
// [ClassA, FunctionA],
// [ClassB, FunctionB],
// [ClassC, FunctionC],
// null_terminator]
// ============================================
// PART 10: WHEN IC MISSES OCCUR
// ============================================
// An IC miss occurs when no cached class matches
// The miss handler:
// 1. Resolves the target for the new class
// 2. Adds the class-target pair to IC data
// 3. Patches the stub if necessary
// 4. Returns the target to the caller
The ten parts trace the lifecycle of a call site from unlinked to monomorphic to polymorphic to megamorphic. The key transition is Part 4, where a second receiver class causes the monomorphic stub to miss and the call site to become polymorphic. Part 6 shows the threshold beyond which the linear scan is replaced by a hash lookup.
Quick Reference
IC States
| State | Dispatch Mechanism | When |
|---|---|---|
| Unlinked | Resolution stub | First execution |
| Monomorphic | Single class check | One receiver class seen |
| Polymorphic | Linear scan of ICData | 2 to max checks classes |
| Megamorphic | Hash table lookup | Exceeds max checks threshold |
ICData Structure
| Field | Purpose |
|---|---|
| Number of arguments checked | How many receiver arguments are tested |
| Class-target groups | Each group: N classes + 1 target function |
| Terminator | Null entry marking end of array |
Specialization Behavior
| State | Unoptimized Dispatch | Optimized Dispatch |
|---|---|---|
| Monomorphic | Class check + jump | Prologue check + jump |
| Polymorphic | Linear scan | Inline each target with guard |
| Megamorphic | Hash table lookup | Hash table lookup stub |
Reload and Debugging
| Event | Effect on IC |
|---|---|
| Hot reload | Resets mono/mega to poly/unlinked |
| Debugger stepping | Resets mono/mega to poly/unlinked |
Best Practices
✅ Do This:
// Keep call sites monomorphic when possible
// Pass the same type to a function consistently
void process(List<int> items) { } // Always List<int> // ✅
// Use abstract classes or interfaces for polymorphic dispatch
// The VM can handle 2-4 classes efficiently in polymorphic state // ✅
// Understand that megamorphic is a fallback
// It prevents unbounded growth but loses frequency info // ✅
❌ Don’t Do This:
// Pass many unrelated types to a dynamic call site
// Forces megamorphic state and slower dispatch // ❌
// Rely on IC state surviving hot reload
// It is reset by design // ❌
// Assume megamorphic is always slow
// It is bounded and predictable, unlike unbounded polymorphic // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Performance degradation after adding classes | Call site crossed polymorphic threshold | Accept megamorphic or reduce class variety |
| IC data grows unbounded | Very high polymorphism | Megamorphic switch prevents this |
| Stale targets after hot reload | IC not reset | VM resets automatically |
| Miss handler called frequently | Call site sees new classes repeatedly | Stabilize receiver types |
Real-World Examples
1. Monomorphic Call Site
void printLength(String s) { print(s.length); }
// Always String, monomorphic check
2. Polymorphic Call Site
void draw(Shape s) { s.draw(); }
// Circle, Square, Triangle — linear scan
3. Megamorphic Call Site
void process(dynamic item) { item.run(); }
// Many unrelated types — hash lookup
4. IC Miss Handler
// Miss: class not in ICData
// Resolve target, add to ICData, return target
5. Hot Reload Reset
// On reload: monomorphic → polymorphic
// Cached targets invalidated
6. ICData Array
[numArgs, [ClassA, TargetA], [ClassB, TargetB], null]
7. Optimized Monomorphic
// Prologue: if receiver.class == expected → jump
8. Optimized Megamorphic
// Hash receiver.class → bucket → target
9. Polymorphic Threshold
// FLAG_max_polymorphic_checks controls switch
10. Type Feedback
// ICData feeds optimizer specialization decisions
Visual
Call Site State Transitions
┌──────────────────────────────────────────────────────────────┐
│ CALL SITE LIFECYCLE │
│ │
│ ┌─────────────┐ │
│ │ UNLINKED │ First execution │
│ │ │ Resolution stub │
│ └──────┬──────┘ │
│ │ resolves target │
│ ▼ │
│ ┌─────────────┐ │
│ │ MONOMORPHIC │ One class seen │
│ │ │ Class check + jump │
│ └──────┬──────┘ │
│ │ different class │
│ ▼ │
│ ┌─────────────┐ │
│ │ POLYMORPHIC │ 2..max checks classes │
│ │ │ Linear scan of ICData │
│ └──────┬──────┘ │
│ │ exceeds max checks │
│ ▼ │
│ ┌─────────────┐ │
│ │ MEGAMORPHIC │ Hash table lookup │
│ │ │ Bounded, no unbounded growth │
│ └─────────────┘ │
│ │
│ Hot reload resets mono/mega to poly/unlinked. │
│ │
└──────────────────────────────────────────────────────────────┘
ICData Array Structure
┌──────────────────────────────────────────────────────────────┐
│ ICData ARRAY │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Header: number of arguments checked (N) ││
│ ├─────────────────────────────────────────────────────────┤│
│ │ Check 1: [ClassA] → FunctionA ││
│ ├─────────────────────────────────────────────────────────┤│
│ │ Check 2: [ClassB] → FunctionB ││
│ ├─────────────────────────────────────────────────────────┤│
│ │ Check 3: [ClassC] → FunctionC ││
│ ├─────────────────────────────────────────────────────────┤│
│ │ Terminator: null → null ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ Stub scans checks sequentially. │
│ First matching class wins. │
│ No match → IC miss. │
│ │
└──────────────────────────────────────────────────────────────┘
Monomorphic vs Polymorphic vs Megamorphic
┌──────────────────────────────────────────────────────────────┐
│ MONOMORPHIC POLYMORPHIC │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ if class == Dog │ │ scan: │ │
│ │ jump Dog.sound│ │ Dog? → yes │ │
│ └─────────────────┘ │ Cat? → yes │ │
│ │ Bird? → yes │ │
│ One comparison. └─────────────────┘ │
│ Fastest. Linear scan. │
│ Depends on count. │
│ │
├──────────────────────────────────────────────────────────────┤
│ MEGAMORPHIC │
│ │
│ ┌─────────────────┐ │
│ │ hash(class) │ │
│ │ → bucket │ │
│ │ → target │ │
│ └─────────────────┘ │
│ │
│ Hash lookup. │
│ Bounded memory, predictable cost. │
│ Loses frequency info for optimizer. │
│ │
└──────────────────────────────────────────────────────────────┘
Optimized Code Specialization
┌──────────────────────────────────────────────────────────────┐
│ MONOMORPHIC IC DATA → OPTIMIZED CODE │
│ │
│ ┌─────────────────┐ │
│ │ Prologue: │ │
│ │ cmp class, Dog │ │
│ │ jne miss │ │
│ │ call Dog.sound │ │
│ └─────────────────┘ │
│ │
│ POLYMORPHIC IC DATA → OPTIMIZED CODE │
│ │
│ ┌─────────────────┐ │
│ │ cmp class, Dog │────► call Dog.sound │
│ │ cmp class, Cat │────► call Cat.sound │
│ │ cmp class, Bird │────► call Bird.sound │
│ │ jmp miss │ │
│ └─────────────────┘ │
│ │
│ Each target inlined behind its own guard. │
│ Order based on frequency from ICData. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| IC purpose | Cache method targets at call sites |
| Unlinked state | First execution, resolution stub |
| Monomorphic | One class, class check + jump |
| Polymorphic | 2..max classes, linear scan of ICData |
| Megamorphic | Exceeds threshold, hash table lookup |
| ICData format | numArgs, class-target groups, terminator |
| Threshold flag | FLAG_max_polymorphic_checks |
| Hot reload | Resets mono/mega to poly/unlinked |
| Optimizer use | Type feedback for specialization |
| Megamorphic trade-off | Bounded cost, lost frequency info |
Key takeaways:
- The IC state determines dispatch cost. Monomorphic is a single comparison. Polymorphic is a linear scan whose cost grows with the number of classes. Megamorphic is a hash lookup that is slower than monomorphic but bounded and predictable .
- ICData stores class-target pairs for polymorphic call sites. The array format groups classes by the target function they dispatch to. The stub scans these groups and jumps to the first match .
- The megamorphic threshold prevents unbounded growth. When a call site exceeds
FLAG_max_polymorphic_checksclasses, the VM switches to a hash table. This bounds memory and scan cost at the price of losing precise frequency information for the optimizer . - The optimizer reads IC data to specialize. Monomorphic call sites become direct calls with a class check. Polymorphic call sites become a sequence of inlined targets, each behind its own guard, ordered by frequency .
- Hot reload resets specialized IC states. Monomorphic and megamorphic call sites return to polymorphic or unlinked state because the cached targets may no longer be valid after code changes .
- Megamorphic is not a failure state. It is a deliberate trade-off for call sites that see many receiver types. The dispatch is slower than monomorphic but faster than a full dynamic lookup, and it prevents the IC data from growing without bound .
Remember: Inline caches are the Dart VM’s answer to dynamic dispatch. The call site caches what it has learned about receiver classes, and the shape of that cache determines how the next call executes. A call site that always sees the same class is fast because it caches one target. A call site that sees a few classes is still fast because the linear scan is short. A call site that sees many classes becomes megamorphic, which is the VM’s way of saying that the call site is inherently polymorphic and no amount of caching will make it monomorphic. The optimizer uses the cache data to generate specialized code, but it can only specialize on what the cache has seen. Understanding IC states explains why passing consistent types to a function is faster than passing many unrelated types, and why the VM resets these caches on hot reload.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!