Three Kinds of Miss, Three Different Fixes
Compulsory, capacity and conflict misses look identical in a counter and have almost nothing in common as problems. Prefetching helps one, blocking helps another, and layout changes help the third — so classifying the miss is what turns a measurement into a plan.
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
The three Cs
The classic taxonomy asks a counterfactual for each miss. Would it have missed in an infinitely large cache? Then it is compulsory — the data had never been fetched and something had to bring it in. Would it have hit in a fully associative cache of the same size? If not, it is capacity — the working set simply exceeds what fits. If it would have hit in a fully associative cache of the same size but missed in the real one, it is conflict — the placement rules, not the capacity, evicted it.
That framing is useful precisely because each counterfactual points at a different lever. Compulsory misses are reduced by fetching earlier or by fetching more per trip, not by having more room. Capacity misses are reduced by needing less at once. Conflict misses are reduced by changing which set an address maps to, which means changing addresses — alignment, padding or stride.
A fourth category is worth naming on multicore machines: coherence misses, where a line was invalidated because another core wrote to it. These are not a capacity or placement failure at all, and the fix is neither more cache nor better layout but reducing sharing (False Sharing: Independent Data, Shared Line, MESI and Its Relatives).
| Kind | Cause | Typical signature | What actually helps |
|---|---|---|---|
| Compulsory | First touch of a line; it was never resident | Scales with data volume, not with reuse | Prefetching, larger effective transfers, denser data |
| Capacity | Working set exceeds the cache | Step change as size grows past a level | Blocking, smaller footprint, fewer live structures |
| Conflict | Too many hot addresses map to one set | Sensitive to stride and alignment; erratic | Change alignment, padding or stride (Tag, Index and Offset: How an Address Finds Its Line) |
| Coherence | Another core wrote the line | Appears only under threading; scales with cores | Reduce sharing; pad separated data (False Sharing: Independent Data, Shared Line) |
Telling them apart in practice
You rarely get a counter that says "conflict". You infer the category from how the miss rate responds to controlled changes, which is a small set of cheap experiments.
Grow the working set gradually and watch for a step: a sharp transition at a size boundary implicates capacity. Change the stride slightly — from a power of two to a nearby odd number, or pad an array by one line — and watch the miss rate move sharply: that is the conflict signature, because you changed which sets are used without changing how much data there is. Run the same data twice and compare the first pass to the second: what remains on the second pass is not compulsory, and what disappears was.
The power-of-two stride case deserves special mention because it is so common and so surprising. Arrays dimensioned to exact powers of two frequently cause many rows to map to the same sets, which is why padding a matrix row by a single element sometimes produces a large speedup with no other change. It looks like superstition and it is Set-Associative Caches: The Compromise That Won arithmetic.
1float a[1024][1024]; // row stride = 4096 bytes2 3for (i in 0..1024)4 sum += a[i][0]; // walk one column5 6// Every element is 4096 bytes apart. With a power-of-two7// stride, these addresses map to very few sets, so they8// evict each other while most of the cache sits unused.1float a[1024][1024 + 1]; // row stride = 4100 bytes2 3for (i in 0..1024)4 sum += a[i][0]; // identical walk5 6// One extra element per row shifts each successive address7// into a different set. Same data, same instructions,8// conflict misses largely gone.Nothing about capacity or access order changed — only which sets the addresses map to. That is the definition of a conflict miss, and padding is the standard fix. It also shows why "add more cache" would not have helped: most of the cache was already idle.
The taxonomy is a model, not a physical fact
The three Cs are defined by counterfactuals against idealised caches, which means a real miss does not carry a label. A miss can be partly capacity and partly conflict — the working set is somewhat too large *and* poorly distributed — and reasonable people classify borderline cases differently. Prefetching further blurs the boundary by converting some compulsory misses into traffic that never appears as a demand miss at all.
This matters because the taxonomy is a thinking tool for choosing an experiment, not a measurement you can report. The productive use is: form a hypothesis about which category dominates, run the cheap experiment that would distinguish it, and act on the result. The unproductive use is arguing about how to classify a miss you have not tried to change.
It generalises well beyond CPU caches, which is part of why it is worth knowing. A database buffer pool has the same three failure modes — first read of a page, working set exceeding the pool, and hot pages mapping awkwardly — and the same three families of fix, which is why The Buffer Pool reasoning feels familiar once you have this framing.
1. FIRST vs SECOND PASS over the same fresh data
second pass still misses -> capacity or conflict
second pass hits -> the first pass was compulsory
2. SWEEP the working-set size, plot time per element
sharp step at a size -> capacity
smooth curve -> not capacity
3. PERTURB the stride: pad one array dimension by a single element
large swing -> conflict
no change -> not conflict
4. VARY thread count, holding per-thread work constant
miss rate grows w/ threads -> coherence, not cache sizing
None of these is a counter you can read directly. The category is
inferred from how the miss rate RESPONDS, which is why the model is
a hypothesis generator rather than a measurement.Key points
- Compulsory, capacity and conflict misses look identical in a counter but have disjoint fixes.
- Compulsory misses respond to prefetching and denser transfers; capacity to blocking; conflict to alignment and stride changes.
- Multicore adds coherence misses, where the fix is reducing sharing rather than anything cache-related.
- Classify by controlled experiment — vary size for capacity, vary stride for conflict, compare first and second pass for compulsory.
- Power-of-two array dimensions are a common conflict-miss trap, and padding by one element is the standard remedy.
Cache Simulator
Change an input and watch which number moves — and which one refuses to.
Almost all hits. Either the working set is resident or the pattern has enough spatial locality that each line pays for many accesses.
Follow the mechanism
The path through the machine, hop by hop — and the conclusions it invites that are wrong.
- 1First touch → compulsory miss: no copy exists anywhere on chip, so the line must be fetched regardless of cache size.
- 2Working set exceeds capacity → eviction before reuse: lines are displaced by other lines simply because there is not room.
- 3Address maps to a busy set → conflict eviction: the line is displaced although other sets have free ways (Tag, Index and Offset: How an Address Finds Its Line).
- 4Another core writes the line → invalidation: your copy is dropped for correctness, not for space (MESI and Its Relatives).
- 5Next access → refetch: in every case the line is re-fetched from further out, at the cost of wherever it now lives.
- • Concluding "we need a bigger cache" from a raw miss rate, when the misses are compulsory and capacity is irrelevant.
- • Applying blocking to a conflict problem, getting a small improvement from the incidental layout change, and stopping there.
- • Assuming a power-of-two array size is optimal because it makes the address arithmetic tidy.
- • Treating the three-Cs classification as something a counter reports rather than something you infer.
Consequences, controls and cost
- • Adding cache capacity produces no improvement when the misses are compulsory or conflict-driven.
- • A tiny change to an array dimension can produce a large, seemingly inexplicable speedup.
- • Blocking a computation whose misses are conflicts yields disappointing results and wasted effort.
- • Threaded code can show a miss rate that grows with core count while single-threaded behaviour is fine.
- • Run the distinguishing experiment before choosing a fix — size sweep, stride perturbation, first-versus-second pass.
- • For capacity: block the computation, shrink the footprint, reduce the number of live hot structures ([[working-set]]).
- • For conflict: pad array dimensions off powers of two, realign hot structures, or change the traversal stride.
- • For compulsory: increase useful bytes per transfer through denser layout, or prefetch where the pattern is predictable.
- • For coherence: separate per-thread data onto distinct lines and reduce genuine sharing ([[false-sharing]]).
- • Sweep the working-set size and look for step changes — a sharp step at a level boundary indicates capacity.
- • Perturb the stride or pad an array by one element and re-measure; a large swing implicates conflict.
- • Compare miss rate on a first pass over fresh data against a second pass over the same data to isolate compulsory misses.
- • For threaded code, compare single-thread and multi-thread miss rates; growth with thread count implicates coherence.
- • Padding to avoid conflicts wastes memory and can push a working set past a level, trading one miss type for another.
- • Blocking adds a machine-specific tuning parameter and makes the code harder to read.
- • Prefetch hints are easy to get wrong and can evict useful data or waste bandwidth when the prediction is poor.
Scope
§224 — what these claims are specific to.
- SIMPLIFIEDThe three-Cs model is a teaching taxonomy defined against idealised caches; real misses are frequently mixed, and prefetching moves work between categories
- GENERALThe same taxonomy and the same families of fix apply to database buffer pools and OS page caches; only the units and capacities change