What the machine does

The cliff where the data stops fitting

Below the cache's capacity, almost every access hits. A factor of eight above it, almost every access misses. The transition is not gradual and it is not a property of any algorithm — it is a property of how much data there is, and an algorithm's complexity class says nothing about which side of it a program is working on.

Take a fixed number of random accesses — twenty thousand, say — into an array of nn elements, and vary nn. The number of accesses does not change. The algorithm does not change. Only the size of the region being touched changes.

The miss rate goes from essentially zero to essentially one, and it does so over about a factor of eight in nn.

The cliff: miss rate against working-set sizeTwenty thousand uniformly random accesses into an array of n elements, replayed through a cache holding 512 elements. Below 512 the miss rate is essentially zero; a factor of eight above it, essentially everything misses. The comparison count of an algorithm says nothing about which side of this cliff it is working on, which is why the two counts are carried separately. Model: fully associative · 64 lines × 8 elements · LRU.0%25%50%75%100%645124,09665,536cache holds 512array size n (elements)miss ratefully associative · 64 lines × 8 elements · LRU20,000 random accesses per point
Fig. 1 Twenty thousand uniformly random accesses into an array of n elements, replayed through a cache holding 512 elements. Below capacity the miss rate is a fraction of a percent. A factor of eight above it, better than 60% of accesses miss, and it keeps climbing. The transition is the single most important shape in this subject.

What the cliff is

The mechanism is not subtle. A cache holds some fixed amount of data. If the set of things a program is actively using — its working set — fits, then after the first pass everything is already there and every subsequent access hits. If the working set does not fit, then by the time the program comes back to something, it has been evicted to make room for everything else, and the access misses.

There is no middle ground to speak of, because eviction is all-or-nothing per line and the accesses are spread uniformly. Slightly over capacity means slightly more than half the lines get evicted before reuse; well over capacity means essentially all of them do.

The practical shape of this: an algorithm can be fast at one size and slow at twice the size, with no change whatsoever in its operation count per element. Every count on this site would report the second run as costing exactly twice the first. A clock would report considerably more than twice.

Why it makes complexity classes misleading

A complexity class is a statement about how the operation count grows. It carries an implicit assumption that operations cost about the same as each other, and across a cliff they do not.

Consider an O(n)O(n) algorithm — one pass over an array, constant work per element. Double nn and the operation count doubles, so the class predicts a doubling of cost. If the doubling crosses the cache capacity, the actual cost can grow by considerably more, because the per-operation cost changed at the same time as the count.

The effect is bounded — the ratio between a hit and a miss is large but finite, so the whole thing is a constant factor and the class survives. But it is a constant factor of ten or more that switches on somewhere in the middle of the range that matters, and “asymptotically it’s still linear” is not much comfort to somebody whose job just got ten times slower.

This is one of the more concrete instances of the general problem with asymptotic reasoning: the notation describes behaviour past some threshold and is silent about where the threshold is. Here the threshold is a hardware parameter that the algorithm’s author never saw.

Two kinds of miss

Not all misses are avoidable, and separating them matters when deciding whether an algorithm can be improved.

Compulsory misses. The first time a cache line is touched, it must be fetched. Nothing can prevent this, and the count is simply the number of distinct lines the algorithm touches. For a single pass over an array of nn elements at eight elements per line, that is n/8n/8, and no algorithm reading all the data can do better.

Capacity misses. Everything else — a line that was fetched, evicted, and needed again. These are the ones an algorithm can avoid, by arranging to finish with data before it gets pushed out.

The site’s model asserts the distinction directly: with a cache larger than the data, the measured miss count must equal the compulsory floor exactly. If it were higher, the model would be inventing evictions that cannot happen; if it were lower, it would be inventing hits.

The distinction also identifies when optimisation is pointless. An algorithm whose misses are nearly all compulsory has nothing left to gain from better locality, and effort should go elsewhere. One whose misses are mostly capacity misses can potentially be restructured — and the standard restructuring is blocking: process the data in chunks that fit in cache, finishing entirely with each chunk before moving on. That is exactly what an insertion-sort cutoff does to merge sort’s recursion, and it is most of why the cutoff exists.

4,096 accesses, five ordersEvery row performs exactly 4,096 array accesses — the same count an operation-counting analysis would assign them all — and the modelled miss counts differ by a factor of 8. Reading backwards is as cheap as reading forwards, because a cache line is a line whichever end you enter it from. Stepping by 8 elements touches a new line every time and is as expensive as random. Model: fully associative · 32 lines × 8 elements · LRU.cache missesstraight through512100% sequentialbackwards5120% sequentialevery 8th element4,0960% sequentialevery 97th element4,0960% sequentialuniformly random3,8460% sequentialfully associative · 32 lines × 8 elements · LRU8× between best and worst order
Fig. 2 The same total access count, five different orders. The two cheapest — straight through and backwards — pay only compulsory misses, one per line. The two most expensive pay a capacity miss on nearly every access. The difference between the extremes is a factor of eight and none of it is visible in an operation count.

Backwards is as cheap as forwards

One row of that figure deserves attention because it contradicts a common intuition.

Reading an array from the last element to the first produces exactly the same number of misses as reading it from first to last. Both touch every line once and each line is used eight times.

The intuition that backwards should be worse comes from thinking about prefetching rather than caching, and it is half right: some older hardware prefetchers only detected ascending strides, so a descending walk got no help. Modern ones handle both directions. The site’s model has no prefetcher at all, so the two are exactly equal in it, and on real hardware they are close.

What the model does capture is the thing that is genuinely fatal: stepping by the line size or more. Stepping by 8 elements when a line holds 8 touches a new line every single time, so a walk of nn accesses costs nn misses instead of n/8n/8. A factor of eight, from an access pattern that looks perfectly regular and is.

This is why the notorious cache pathologies involve strides that are powers of two — they interact badly with both line size and set indexing, and the resulting slowdowns look inexplicable from the source code.

What the cliff does to a complexity comparison

The most common way the cliff produces a wrong conclusion is in an informal benchmark, and the mechanism is worth spelling out because it is easy to fall into.

Suppose two algorithms are compared at n=10,000n = 10{,}000 and one is 30% faster, and the conclusion drawn is that it is the better algorithm. Now suppose the faster one’s working set at that size fits in the last-level cache and the slower one’s does not, because it keeps a buffer. At n=1,000,000n = 1{,}000{,}000 neither fits, both are paying full price for every access, and the comparison count reasserts itself as the thing that matters — possibly reversing the verdict.

The reverse also happens. An algorithm that looks bad at small nn because of a fixed setup cost can win decisively at large nn, and an algorithm that looks good at small nn because everything fits in cache can lose.

There is no way to detect this from a single measurement, and the fix is not subtle: measure across a range, and plot it. A curve that bends where a curve should be straight is the cliff announcing itself, and it is invisible in any single number.

This site’s fits run over three orders of magnitude for exactly this reason, and the one case where the fitted class changed when the range was extended is the same lesson arriving from the other direction.

Comparison counts against n, random inputMeasured counts on logarithmic axes, where a power law is a straight line and its exponent is the slope. The quadratic algorithms rise at twice the gradient of the linearithmic ones, and the vertical gaps between the parallel lines are the constants the notation discards.10010³10010³10⁴10⁵ncomparisonsMergeHeapsorta power law is a straight line herecomparisons, counted exactly
Fig. 3 What a comparison across a range looks like when nothing dramatic happens. Merge sort and heapsort stay parallel over three orders of magnitude, so the ratio between them is stable and a measurement at any one size would have given the same answer. That is the situation to hope for and never to assume.

Where the cliff puts the sorting algorithms

The cliff explains a specific and otherwise puzzling observation: recursive divide-and-conquer algorithms have an advantage on large data that has nothing to do with their operation counts.

Merge sort and quicksort both split the problem repeatedly. After a few levels of recursion, the subproblem they are working on fits in cache, and everything below that level runs entirely at cache speed. The top few levels cost misses; the bottom many levels cost almost none.

Heapsort does the opposite. It maintains one structure the size of the whole input and jumps around inside it from the first operation to the last. There is no point at which its working set becomes small. That is why its modelled miss count at n = 2,048 is 4,677 against merge sort’s 1,277 — 3.7 times, on only 1.9 times the comparisons.

This is the property that made cache-oblivious algorithms an interesting idea: a recursive algorithm that halves its problem automatically becomes cache-friendly at every level of a hierarchy it knows nothing about, because at some level of the recursion each cache size is matched. Merge sort gets this for free by being recursive.

Comparisons against modelled cache misses, n = 2048One point per algorithm, both axes logarithmic. If the comparison count determined the memory behaviour the points would fall on a line, and they do not: Merge sort and Quicksort, median-3 and Merge + cutoff sit at least two places apart in the two rankings. Cache model: fully associative · 64 lines × 8 elements · LRU. The vertical axis is a modelled miss count, not a time.10⁵10⁶10³10⁴10⁵comparisonscache misses (modelled)Insertion sortSelection sortBubble sortMerge sortHeapsortQuicksort, firstQuicksort, median-3Quicksort, randomShellsortMerge + cutofffully associative · 64 lines × 8 elements · LRUa modelled count, not a time
Fig. 4 The sorting algorithms placed by both counts. The horizontal position is comparisons and the vertical is modelled misses, both logarithmic. Heapsort sits noticeably above the diagonal trend and quicksort below it — those displacements are the cliff at work, and they are what makes the two rankings differ.

Two sizes, and a third that is often forgotten

The cliff described here is one transition, and a real machine has several — one per level of the hierarchy. A program’s working set can fit in L2 and not L1, fit in L3 and not L2, or fit in memory and not in L3, and each boundary has its own cliff with its own height.

The site’s model has one level, so it shows one cliff. That is a simplification and it is the right one for the argument being made: the shape is identical at every level and only the capacity and the penalty change.

There is a third transition, further out, that behaves differently enough to be worth naming. When the working set exceeds physical memory the operating system starts paging to disk, and the penalty is not a factor of a hundred but a factor of ten thousand or more. Every effect on this page becomes correspondingly more dramatic, and an algorithm’s access pattern stops being an optimisation and becomes the entire question. The external-memory algorithms literature exists for exactly this regime, and its cost model counts block transfers rather than operations — which is the same move this site makes with modelled misses, taken seriously.

The cliff: miss rate against working-set sizeTwenty thousand uniformly random accesses into an array of n elements, replayed through a cache holding 1024 elements. Below 1024 the miss rate is essentially zero; a factor of eight above it, essentially everything misses. The comparison count of an algorithm says nothing about which side of this cliff it is working on, which is why the two counts are carried separately. Model: fully associative · 128 lines × 8 elements · LRU.0%25%50%75%100%645124,09665,536cache holds 1024array size n (elements)miss ratefully associative · 128 lines × 8 elements · LRU20,000 random accesses per point
Fig. 5 The cliff with twice the capacity. Its position moves right by a factor of two and nothing else about it changes — which is why one modelled level is enough to make the argument, and why the model’s parameters have to be printed for the numbers to mean anything.

What this means for measurement

Three practical consequences, and they are the reason this essay exists in a site that never publishes a timing.

A benchmark at one size measures one side of the cliff. Timing a sort on ten thousand elements and concluding anything about a million is extrapolating across a transition. This is the most common single mistake in informal benchmarking, and it can reverse the conclusion.

The cliff moves between machines. Capacity is a hardware parameter, so where the transition falls depends on the processor. A result obtained on a machine with a large last-level cache may not hold on one with a small one, even at identical nn. A comparison-count result holds everywhere.

The model has to state its parameters or its numbers mean nothing. A miss count without a line size and a capacity is not a measurement. Every figure on this site that reports one prints them, for exactly this reason: the number is only interpretable against the model that produced it, and the model is a choice.

Where each algorithm looks, and whenEvery array access from one run of each algorithm on 256 random elements: time along the horizontal axis, array index up the vertical. Quicksort, median of three makes 39% of its accesses to the next element or the same one; Heapsort makes 15%. That difference is invisible in the comparison count and is most of what the machine feels.Quicksort39% sequential · 6,999 accesses2560Heapsort15% sequential · 14,044 accesses2560time (accesses, left to right) · index (bottom to top)one run each, n = 256every access plotted
Fig. 6 Quicksort against heapsort, every access from one run of each. Quicksort’s pattern narrows as the recursion descends — the triangular shape is subproblems getting smaller, which is exactly the structure that eventually fits in cache. Heapsort’s spans the whole array from beginning to end and never narrows.

The honest limit

Everything above is measured through a model with no prefetcher, one level, and no associativity conflicts. The cliff it shows is real and the shape is right; the exact miss counts are not what a real processor would produce.

What survives the modelling assumptions is the ordering — that a strided walk is much worse than a sequential one, that heapsort is much worse than merge sort, that the transition is sharp — because every simplification in the model applies to all the algorithms equally. What does not survive is any attempt to turn these numbers into predicted times, and no essay here makes one.

The cliff is the reason operation counts and running times diverge, and knowing where it falls for a particular workload is worth more than knowing that workload’s complexity class — because which side of the cliff a program sits on is usually changeable, and the class rarely is.