Performancemlpoutstanding missesoverlapdependencylatency hiding

Misses That Overlap Are Nearly Free

A cache miss costs a great deal if the core has nothing else to do, and almost nothing if it does. Modern cores keep several misses outstanding at once, so ten independent misses can cost barely more than one — while ten dependent misses cost ten times as much. This is why miss counts alone never predict runtime.

Follow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
Why do two loops with the same number of cache misses take completely different amounts of time?
What you wrote
Both loops miss the last-level cache roughly a million times, so both should pay roughly a million times the miss penalty and take about the same time.
What the hardware does
The first loop issued its misses independently and the core kept a dozen in flight simultaneously; the second could not compute the next address until the previous load returned, so every miss was paid in full, one after another.
Memory-level parallelism is the mechanism that explains why pointer chasing is catastrophically slower than array traversal despite similar miss counts, and why prefetching helps some loops enormously and others not at all.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

The core does not stop at the first miss

When a load misses, the core does not halt. Out-of-order execution lets it continue past the missing instruction, issuing any subsequent work whose operands are available — including further loads, which may themselves miss. The memory subsystem tracks each outstanding fill in a dedicated structure, and several can be in flight simultaneously. The result is that their latencies overlap rather than accumulate.

That overlap is bounded by two things. The first is the size of the structure tracking outstanding fills — once it is full, the core cannot issue another miss no matter how much independent work is available. The second is the reorder buffer: the core can only look so far ahead for independent work, and if the next independent load is beyond that horizon it might as well not exist. Both limits are firmly microarchitecture-specific.

The practical consequence is that "cost per miss" is not a constant. It ranges from nearly the full memory latency, when misses are serialized, down to a small fraction of it, when many overlap. Any performance model that multiplies a miss count by a fixed penalty will be badly wrong in one direction or the other.

Relative cost of eight last-level misses, as a function of how many the core can overlap. Unitless by design — the ratio transfers between machines, the nanoseconds do not. — 1 unit ≈ the latency of one isolated last-level missSIMPLIFIED
8 misses, fully serialized (each address depends on the last)×8
8 misses, 2 overlapping×4
8 misses, 4 overlapping×2
8 misses, all 8 overlapping×1.2
Ratios, not times. Absolute latencies depend on the processor, its clock, the memory it is attached to and what else is running — publishing them would be wrong everywhere except one machine. The bars are log-scaled, so each step is larger than it looks.
8 misses, fully serialized (each address depends on the last)pointer chasing; the worst case and a common one
8 misses, 2 overlappingshort dependency chains, partial overlap
8 misses, 4 overlappingtypical of a loop with a couple of independent streams
8 misses, all 8 overlappingindependent, prefetchable, contiguous — near best case

Dependency is what destroys it

The condition for overlap is independence: the core must be able to compute the second address without waiting for the first load to return. Array traversal satisfies this trivially — every address is base plus index, computable immediately, so the core can run far ahead and issue many loads. Pointer chasing violates it absolutely: node->next cannot be known until node has arrived, so each miss must fully complete before the next can even be issued.

This is the real reason a linked list traversal is so much slower than an array traversal at the same asymptotic complexity, and it is a sharper explanation than "the array has better locality". Locality matters, but even a linked list whose nodes happen to be laid out contiguously suffers, because the *dependency* remains: the hardware prefetcher has nothing to predict from and the core has nothing independent to overlap. Pointer Chasing: The Address You Do Not Have Yet and Both Are O(n). One Is Far Slower. develop this from the data-structure side.

Because the constraint is dependency rather than data volume, the fixes are structural. Traversing several independent lists at once interleaves their chains and restores overlap. Storing indices into a contiguous array instead of pointers lets the core compute addresses ahead of the loads. Splitting one long chain into several shorter parallel ones converts a latency problem into a bandwidth one, which is a much better problem to have.

One chain — misses serialize, MLP is 1
1# The next address is inside the value being loaded.
2while (node) {
3 sum += node->value;
4 node = node->next; # cannot issue until this load returns
5}
6# 8 misses cost roughly 8 x miss latency
Four chains interleaved — misses overlap, MLP is 4
1# Four independent traversals; the core can have
2# four loads outstanding at once.
3while (a || b || c || d) {
4 if (a) { sum += a->value; a = a->next; }
5 if (b) { sum += b->value; b = b->next; }
6 if (c) { sum += c->value; c = c->next; }
7 if (d) { sum += d->value; d = d->next; }
8}
9# 8 misses cost roughly 2 x miss latency

Identical work, identical miss count, roughly a quarter of the stall time. Nothing about locality changed — what changed is how many independent misses the core could keep in flight at once.

What this means for prefetching and for measurement

Hardware prefetching is, in effect, a machine for manufacturing memory-level parallelism. It detects a regular access pattern and issues loads ahead of demand, so the data is already arriving when the core asks. That works precisely because the addresses are predictable without waiting for previous results — the same independence condition. It is also why prefetching does nothing for pointer chasing: there is no pattern to extrapolate, since the next address is data the prefetcher does not yet have. Prefetching: The Hardware Guesses What You Will Read Next covers the mechanism.

For measurement, the immediate implication is that a miss count is not a cost and must never be reported as one. Two functions with identical LLC miss counts can differ several-fold in runtime, and the counter that distinguishes them is stall cycles attributable to memory, not the miss count itself. This is the specific trap behind so many "we reduced cache misses by 30% and nothing got faster" reports.

The reverse also happens and is less well known: a change that *increases* miss count while increasing overlap can be a net win. Converting a dependent traversal into several independent streams may touch more memory and miss more often, and still finish sooner because the misses now happen concurrently. Optimising the miss counter is not the goal; optimising cycles is.

Why two loops with the same miss count diverge
PropertyHigh MLP loopLow MLP loop
Address computationIndependent of loaded valuesDepends on the previous load's result
Misses in flightSeveral, up to the hardware limitOne
Hardware prefetchEffective — pattern is extrapolableUseless — no pattern to predict
Cost per missA fraction of full latencyApproximately full latency
Typical shapeArray scan, matrix traversal, streamingLinked list, tree descent, hash chain walk
The fixAlready near best caseBreak the chain: interleave, use indices, restructure

Key points

  • Cores keep multiple cache misses outstanding, so independent misses overlap and cost far less than their sum.
  • The limit is set by the structure tracking outstanding fills and by how far ahead the reorder buffer can look — both microarchitecture-specific.
  • Dependent addresses destroy overlap entirely: pointer chasing sustains a memory-level parallelism of one.
  • Hardware prefetching works by manufacturing this overlap, which is why it helps regular patterns and does nothing for chained loads.
  • A miss count is not a cost; two loops with equal miss counts can differ several-fold in runtime.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Load → miss: a load misses the last-level cache and an outstanding-fill entry is allocated for it.
  2. 2
    Core → continue: out-of-order execution proceeds past the missing load to any instruction whose operands are ready.
  3. 3
    Independent load → second miss: another miss issues while the first is still in flight, and its latency overlaps.
  4. 4
    Structure full → stall: once all outstanding-fill entries are occupied, no further miss can issue regardless of available work.
  5. 5
    Dependent load → serialization: if the next address requires the previous result, no overlap is possible and each latency is paid in full.
What people conclude from this — wrongly
  • Multiplying miss count by a fixed penalty to estimate cost, which is wrong by several times in both directions.
  • Concluding an optimisation failed because misses fell and runtime did not, without checking whether those misses were overlapping.
  • Assuming prefetching will rescue a pointer-heavy traversal.
  • Rejecting a restructuring because it increased total misses, when it increased overlap by more.

Consequences, controls and cost

What it causes
  • • Linked structures perform far worse than their miss counts suggest, while array scans perform far better.
  • • Reducing cache misses can fail to improve runtime if the removed misses were already overlapping.
  • • Restructuring to increase independent accesses can improve runtime while increasing total misses.
  • • Prefetch-friendly access patterns get a compounding benefit: fewer misses and better overlap of the ones that remain.
What you can do
  • • Break dependency chains: interleave several independent traversals so the core has misses to overlap.
  • • Replace pointers with indices into contiguous storage so addresses are computable before the data arrives.
  • • Prefer layouts the hardware prefetcher can extrapolate — sequential or fixed-stride — over unpredictable indirection.
  • • When a chain is irreducible, accept it and optimise elsewhere; some traversals are genuinely latency-bound and nothing local will fix them.
How to see it
  • • Stall cycles attributable to memory, alongside the miss count — the pair is the diagnosis, neither alone is.
  • • Average outstanding fill occupancy where the chip exposes it, which is the most direct read on achieved MLP.
  • • A controlled experiment: interleave two independent instances of the traversal and see whether throughput nearly doubles.
  • • Achieved bandwidth — a latency-bound chained traversal uses very little of it, which is itself the tell.
What it costs
  • • Interleaving independent chains complicates the code substantially and increases register and cache pressure.
  • • Index-based structures lose the type safety and convenience of pointers and can complicate ownership.
  • • Restructuring for overlap can increase total memory traffic, which is a poor trade on a bandwidth-saturated system.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • MICROARCH-SPECIFICThe number of outstanding misses a core can sustain, and the reorder buffer depth that determines how far ahead it can find independent work, are design parameters that differ substantially between cores and generations.
  • SIMPLIFIEDThe cost scale shows how overlap changes aggregate cost under idealised assumptions; real overlap is partial and depends on the surrounding instruction mix.

Misconceptions

Claim
“Each cache miss costs the memory latency, so cost is misses times latency.”
Reality
Only when misses are serialized. Independent misses overlap, and a loop sustaining several in flight pays a small fraction of the nominal per-miss cost.
Claim
“A linked list is slow because its nodes are scattered in memory.”
Reality
Scattering hurts, but the deeper problem is dependency: the next address lives inside the value being loaded, so no overlap and no prefetching is possible even if the nodes happen to be contiguous.
Claim
“Fewer cache misses always means a faster program.”
Reality
A restructuring that increases misses while making them independent can be substantially faster, because overlap matters more than count.

Apply it