Counting

The sort that makes none of them

Every count on this collection is a count of comparisons, swaps, reads or writes, and radix sort makes zero of the first. On 65,536 keys it moves five times less data than merge sort, misses the cache three times more, and sits 954,037 comparisons under the floor no comparison sort can go beneath — which is not an achievement, because the floor was never a statement about it.

One run, four counts, four answers made the case that the question “how many operations” has no answer until the operation is named, and named four: comparisons, swaps, reads, writes. Every plate since has reported at least one of them, and the first is the one nearly all of them report.

There is a sort that makes none of the first. Radix sort distributes keys by one digit at a time, from the least significant up, and after the last digit the array is sorted. It never asks which of two keys is larger. Counted by the instrument this collection was built around, it makes zero comparisons — not few, not a constant factor fewer, none — and the floor under every comparison sort says nothing about it, because the floor is an argument about how much one comparison can reveal and it makes no comparisons.

That sounds like a way past a limit and it is not one. This page counts what it does instead.

How it is counted here

The instrument needs one addition and it is worth naming, because a sort that makes no comparisons is a sort the counted array was not designed to watch.

Counting instead of timing is the array that increments a tally each time it is read, written, compared or swapped. Radix sort reads every key twice a pass, writes it once into a scratch array and once back, and compares nothing — so three of the four counters work unchanged and the fourth reports zero honestly.

The scratch array is the part that needed care. A radix pass writes each key to a position given by its digit’s running counter, and if those writes are not in the trace then the one thing this page is about — where the writes land — is invisible. So the scratch array’s addresses go into the same trace as the input’s, offset by nn, and the cache model sees the whole of what a pass touches rather than half of it. A version that traced only the input would have reported radix sort as almost perfectly sequential, which is the shape of mistake the count that came from somewhere else is about: a count that is exact about what it measures and silent about what it does not.

The four counts, side by side

Radix sort moves 5.1 times less data than merge sort and misses the cache 3.0 times more — and makes none of the 965,752 comparisons the floor of 954,037 says merge sort mustReads and writes, and cache misses through 64 lines of 8 elements, for 65,536 32-bit keys sorted four ways. Radix sort, 8-bit digits: 0 comparisons, 786,432 reads and writes, 369,510 misses. Merge sort: 965,752 comparisons, 4,028,656 reads and writes, 122,779 misses. Heapsort: 1,895,405 comparisons, 7,752,850 reads and writes, 645,616 misses. Quicksort, median-of-three: 1,187,435 comparisons, 3,637,451 reads and writes, 105,747 misses. The information floor for sorting 65,536 distinct keys by comparison is 954,037 comparisons, and it says nothing about a sort that makes none.reads and writescache missesRadix sort, 8-bit digitsno comparisonsMerge sort965,752 comparisonsHeapsort1,895,405 comparisonsQuicksort, median-of-three1,187,435 comparisons65,536 keys of 32 bitsdark: the sort that compares nothing
Fig. 1 Reads and writes, and cache misses through 64 lines of 8 elements, for 65,536 32-bit keys sorted four ways. Radix sort with 8-bit digits: no comparisons, 786,432 reads and writes, 369,510 misses. Merge sort: 965,752 comparisons, 4,028,656 reads and writes, 122,779 misses. Heapsort: 7,752,850 reads and writes, 645,616 misses. Median-of-three quicksort: 3,637,451 and 105,747. The information floor for sorting 65,536 distinct keys by comparison is 954,037 comparisons.

Three numbers and three different answers.

In comparisons, radix sort wins by not playing. Merge sort makes 965,752, within 1.2% of the 954,037 the floor demands, which is as close as any method gets. Radix sort makes none. Every plate that reports comparisons reports a zero for it, and a ranking by comparisons puts it first by an infinite margin — which is as clear a demonstration as there is that a ranking is a statement about its unit.

In data moved, radix sort wins by a factor of five. 786,432 reads and writes against merge sort’s 4,028,656. Four passes over the array, each reading every key twice and writing it twice, against merge sort’s seventeen levels of recursion each copying the array in and out.

In cache misses, it loses by a factor of three. 369,510 against merge sort’s 122,779 — and that is the count that decides, because this is a sort of a large array and a miss is worth hundreds of the other operations.

Why it misses

The reason is in the shape of the pass rather than in its size. A pass counts how many keys have each digit value, turns the counts into starting positions, and then walks the array writing each key to its digit’s position. With 8-bit digits there are 256 such positions, each advancing independently, so a pass writes to 256 separate places at once. A cache of 64 lines cannot hold 256 streams, so nearly every write is a miss.

Merge sort’s writes, by contrast, are two streams merged into one — three places, all advancing forward. Where an algorithm looks is the argument that the access pattern is the thing to measure rather than the operation count, and radix sort is the cleanest case on the collection: fewer operations, more misses, and the difference is entirely in how many places at once.

That also says which dial to turn, and turning it is not simple.

Widening the digit from 1 bit to 16 takes the passes from 32 to 2 and the data moved from 6,291,456 to 393,216, and the counters from 2 to 65,536Reads and writes, cache misses and counter words against the digit's width in bits, on logarithmic axes, for 65,536 32-bit keys. 1-bit digits: 32 passes, 6,291,456 reads and writes, 1,310,735 misses — 20.00 an element — 2 counter words, and 0 comparisons. 2-bit digits: 16 passes, 3,145,728 reads and writes, 655,381 misses — 10.00 an element — 4 counter words, and 0 comparisons. 4-bit digits: 8 passes, 1,572,864 reads and writes, 329,867 misses — 5.03 an element — 16 counter words, and 0 comparisons. 8-bit digits: 4 passes, 786,432 reads and writes, 369,510 misses — 5.64 an element — 256 counter words, and 0 comparisons. 11-bit digits: 3 passes, 589,824 reads and writes, 294,912 misses — 4.50 an element — 2,048 counter words, and 0 comparisons. 16-bit digits: 2 passes, 393,216 reads and writes, 196,608 misses — 3.00 an element — 65,536 counter words, and 0 comparisons.124811161010010³10⁴10⁵10⁶digit width, bitswords · misses · countersreads and writescache missescounter words65,536 keys of 32 bitsand no comparisons at any setting
Fig. 2 Reads and writes, cache misses and counter words against the digit’s width, for 65,536 32-bit keys, on logarithmic axes. One-bit digits: 32 passes, 6,291,456 reads and writes, 1,310,735 misses, 2 counters. Four-bit: 8 passes, 1,572,864, 329,867, 16. Eight-bit: 4 passes, 786,432, 369,510, 256. Sixteen-bit: 2 passes, 393,216, 196,608, 65,536. No comparisons at any setting.

The data moved falls monotonically with the digit — it is 2w/b2 \cdot \lceil w/b \rceil passes over the array, so doubling the digit halves it. The misses do not. They fall to 329,867 at four-bit digits, rise to 369,510 at eight, and fall again to 196,608 at sixteen.

The non-monotonicity is the two effects crossing. Fewer passes means fewer chances to miss; more streams means more of each pass’s writes miss. At four bits there are sixteen streams and they fit in the cache, so a pass is nearly sequential and costs little; at eight bits there are 256 and none of them fit, so every write misses and there are still four passes; at sixteen bits every write misses too, but there are only two passes.

Widening the digit from 1 bit to 16 takes the passes from 32 to 2 and the data moved from 393,216 to 24,576, and the counters from 2 to 65,536Reads and writes, cache misses and counter words against the digit's width in bits, on logarithmic axes, for 4,096 32-bit keys. 1-bit digits: 32 passes, 393,216 reads and writes, 81,936 misses — 20.00 an element — 2 counter words, and 0 comparisons. 2-bit digits: 16 passes, 196,608 reads and writes, 40,983 misses — 10.01 an element — 4 counter words, and 0 comparisons. 4-bit digits: 8 passes, 98,304 reads and writes, 20,534 misses — 5.01 an element — 16 counter words, and 0 comparisons. 8-bit digits: 4 passes, 49,152 reads and writes, 23,157 misses — 5.65 an element — 256 counter words, and 0 comparisons. 11-bit digits: 3 passes, 36,864 reads and writes, 18,428 misses — 4.50 an element — 2,048 counter words, and 0 comparisons. 16-bit digits: 2 passes, 24,576 reads and writes, 12,287 misses — 3.00 an element — 65,536 counter words, and 0 comparisons.124811161010010³10⁴10⁵digit width, bitswords · misses · countersreads and writescache missescounter words4,096 keys of 32 bitsand no comparisons at any setting
Fig. 3 The same sweep on sixteen times fewer keys: 4,096 of them, still 32 bits wide. Four-bit digits miss 5.01 times per element, eight-bit 5.65, sixteen-bit 3.00 — the same three numbers, to two decimal places, as on 65,536 keys.

The misses per element do not move with the array’s size at all: 5.03, 5.64 and 3.00 at 65,536 keys against 5.01, 5.65 and 3.00 at 4,096. That is what makes the previous plate a statement about the method rather than about one experiment. A pass’s miss rate is decided by how many streams it writes to against how many the cache holds, and neither of those is nn.

And the sixteen-bit setting’s counters are 65,536 words — as many as the keys. That is the space this sort spends and no comparison sort does, and it is what makes the bottom of the miss curve unreachable in practice: a sort whose counter array is the size of its input has stopped being a cheap way to avoid comparisons.

The term that is not there

Radix sort's reads and writes grow with the keys and not with their number of orderings: 12,288 at 1,024 keys and 3,145,728 at 262,144, against merge sort's 38,352 and 18,211,200Reads and writes against the number of 32-bit keys, on logarithmic axes, for radix sort with 8-bit digits and for three comparison sorts. 1,024 keys: radix 12,288 with no comparisons; Merge sort 38,352 with 8,936 comparisons, Heapsort 71,958 with 17,313 comparisons, Quicksort, median-of-three 35,057 with 11,589 comparisons. 4,096 keys: radix 49,152 with no comparisons; Merge sort 186,256 with 43,976 comparisons, Heapsort 353,782 with 85,733 comparisons, Quicksort, median-of-three 164,249 with 56,469 comparisons. 16,384 keys: radix 196,608 with no comparisons; Merge sort 876,126 with 208,687 comparisons, Heapsort 1,676,094 with 408,247 comparisons, Quicksort, median-of-three 819,291 with 267,187 comparisons. 65,536 keys: radix 786,432 with no comparisons; Merge sort 4,028,656 with 965,752 comparisons, Heapsort 7,752,850 with 1,895,405 comparisons, Quicksort, median-of-three 3,637,451 with 1,187,435 comparisons. 262,144 keys: radix 3,145,728 with no comparisons; Merge sort 18,211,200 with 4,387,008 comparisons, Heapsort 35,194,174 with 8,627,815 comparisons, Quicksort, median-of-three 16,686,075 with 5,644,691 comparisons.1,0244,09616,38465,536262,14410⁵10⁶10⁷keysreads and writesRadix sort, 8-bit digitsMerge sortHeapsortQuicksort, median-of-three32-bit keys, 8-bit digitsradix makes no comparisons at any size
Fig. 4 Reads and writes against the number of 32-bit keys, on logarithmic axes, for radix sort with 8-bit digits and for three comparison sorts. At 1,024 keys: radix 12,288, merge sort 38,352. At 262,144: radix 3,145,728, merge sort 18,211,200. Radix sort’s line is straight with slope one; the comparison sorts’ bend upward.

The two lines have different shapes and the difference is the whole of what the floor is about.

A comparison sort’s count has a logn\log n in it because log2n!\log_2 n! is how many bits it takes to name one of n!n! orderings and a comparison supplies one bit. Radix sort’s has no such term: it makes w/b\lceil w/b \rceil passes over nn keys whatever nn is, so its work is 2nw/b2n \lceil w/b \rceil and its line is straight.

The logn\log n has been replaced by ww, the key’s width, and that is the trade rather than a saving. Sorting nn distinct keys requires them to be distinguishable, so wlog2nw \ge \log_2 n — there is no such thing as a million distinct 8-bit keys. The two costs are therefore comparable by construction, and the reason radix sort looks better here is that 32-bit keys are wider than log265,536=16\log_2 65{,}536 = 16 bits: the sort is paying for sixteen bits of key that carry no information about this particular input.

The floor moves when the question does made the general point that a lower bound belongs to the question rather than to any algorithm. This is the sharper form: a lower bound belongs to a model, and the comparison model’s floor binds every algorithm that only compares and no algorithm that does anything else. A sort that reads a key’s bits is not cheating; it is answering a different question, about keys that have bits, and the floor for that question is a different floor.

Radix sort's reads and writes grow with the keys and not with their number of orderings: 24,576 at 1,024 keys and 6,291,456 at 262,144, against merge sort's 38,352 and 18,211,200Reads and writes against the number of 32-bit keys, on logarithmic axes, for radix sort with 4-bit digits and for three comparison sorts. 1,024 keys: radix 24,576 with no comparisons; Merge sort 38,352 with 8,936 comparisons, Heapsort 71,958 with 17,313 comparisons, Quicksort, median-of-three 35,057 with 11,589 comparisons. 4,096 keys: radix 98,304 with no comparisons; Merge sort 186,256 with 43,976 comparisons, Heapsort 353,782 with 85,733 comparisons, Quicksort, median-of-three 164,249 with 56,469 comparisons. 16,384 keys: radix 393,216 with no comparisons; Merge sort 876,126 with 208,687 comparisons, Heapsort 1,676,094 with 408,247 comparisons, Quicksort, median-of-three 819,291 with 267,187 comparisons. 65,536 keys: radix 1,572,864 with no comparisons; Merge sort 4,028,656 with 965,752 comparisons, Heapsort 7,752,850 with 1,895,405 comparisons, Quicksort, median-of-three 3,637,451 with 1,187,435 comparisons. 262,144 keys: radix 6,291,456 with no comparisons; Merge sort 18,211,200 with 4,387,008 comparisons, Heapsort 35,194,174 with 8,627,815 comparisons, Quicksort, median-of-three 16,686,075 with 5,644,691 comparisons.1,0244,09616,38465,536262,14410⁵10⁶10⁷keysreads and writesRadix sort, 4-bit digitsMerge sortHeapsortQuicksort, median-of-three32-bit keys, 4-bit digitsradix makes no comparisons at any size
Fig. 5 The same comparison with four-bit digits rather than eight — eight passes rather than four, and the setting with the fewest misses among the narrow digits. Radix sort moves 24,576 words at 1,024 keys and 6,291,456 at 262,144, against merge sort’s 38,352 and 18,211,200. The straight line is still straight and it is now only three times below merge sort’s rather than five.

Both settings give a straight line and the gap between them is a constant, which is the point: the digit width is a constant factor on radix sort’s work and the key width is the term. Changing the digit moves the line up and down; changing nn moves along it; and only changing the key’s width changes its slope, which it does not, because the slope is one.

What it gives up

Two things, and both are preconditions rather than costs.

The keys must have digits. Every comparison sort on this collection works on anything with a total order — strings, records, objects with a comparator — and learns about its input only through that comparator, which is what the adversary who knows the seed exploits and what makes the floor argument possible at all. Radix sort must be able to take a key apart. A sort of user objects by a caller-supplied comparator cannot be a radix sort, whatever the counts say.

And the width must be known. The passes are w/b\lceil w/b \rceil and ww has to be decided before the first pass. Sorting 32-bit keys with a 16-bit width fails: the array comes back sorted by its low sixteen bits, which is not sorted, and the check at the end of every run on this collection catches it. A comparison sort has no such parameter, and the fact that radix sort has one is why it sits in library code for integers and nowhere else.

There is a third property, and it goes the other way. Radix sort is stable, necessarily and for free: each pass distributes stably and the stability of the earlier passes is what makes the later ones correct. The order equal keys keep measured what stability costs the sorts that do not have it — nn words of tag and between 0.05 and 2.4 times their comparisons — and radix sort is the one method here where it is not a choice.

Comparisons and swaps at n = 512Selection sort performs the most comparisons of any algorithm here and among the fewest swaps — it never moves an element it does not have to. Ordering these algorithms by comparisons and ordering them by swaps gives two different orders, which is why the question "how many operations" needs the operation named before it has an answer.comparisonsswapsInsertion sort63,071 / 0Selection sort130,816 / 504Bubble sort129,688 / 62,563Merge sort3,964 / 0Heapsort7,653 / 4,170Quicksort5,049 / 2,380n = 512, random inputcounted in the same run
Fig. 6 The count this collection opens with, for context: comparisons and swaps for the sorts that make them, at 512 elements. Every algorithm on that plate has two bars. Radix sort would have one bar at zero and one bar at zero, and would sit on the axis with nothing to compare.

That is not a rhetorical point. A plate ranking sorts by comparisons cannot contain radix sort, and a plate that contained it would be a plate whose scale was set by the algorithms that do compare. The methods that share a counter are the methods that can be drawn together, and a collection that measures by counting operations has a boundary at exactly the point where an algorithm stops performing them.

What this says about the counters

The six counts the count somebody chose measured rank ten sorting algorithms ten different ways, and the argument there was that there is no ranking, only six. Radix sort adds a stronger case: a method can score zero on the count that every other method is compared by, and the zero means it is outside the comparison rather than ahead of it.

That is a general hazard with a counter, and it is worth stating in its general form. A count measures the operations an algorithm performs; a method that performs none of them reports zero; and a zero in a column looks like a win. The repair is the one already in use everywhere here — report several counts, and name the model each floor is stated in — and the reason the repair matters is that the single most quoted fact about sorting, the nlognn \log n floor, is true of the model and not of the problem.

Where it is actually used, and where it is not

The counts above explain a distribution of practice that would otherwise look arbitrary.

Integer keys in a column store: radix. A column of 32-bit identifiers is exactly the case — the keys have digits, the width is known, the array is large enough that four passes over it is cheaper than seventeen levels of merge, and the stability is wanted because the column is being sorted alongside others. Database sorts of this shape are radix sorts and have been for decades.

Anything with a caller-supplied comparator: not radix. A library’s general sort cannot take a key apart, and every library’s general sort is therefore a comparison sort. That is not a performance decision; it is the only thing the interface allows, which is why the sort the library ships measures four comparison sorts and no fourth family.

Short strings: sometimes. A string is a sequence of digits and a most-significant-digit radix sort over it is a real method, used for suffix sorting and for text indexes. The pass count is then the longest key rather than a fixed width, which is a different arithmetic and one this page does not draw.

The dividing line is the one the counts draw, and it is a line about the interface rather than about the cost: a sort is a candidate for a radix sort exactly when the key’s width is known and small against log2n\log_2 n, and it is a candidate for nothing else when the key is only comparable. Neither condition has anything to do with which is faster, and a benchmark comparing the two is comparing methods that are not available to the same caller.

What is not measured here

One key distribution and one width. The keys are 32-bit values from a multiplicative sequence, near-uniform over their range. Keys clustered in a narrow range leave the high digits constant, and a radix sort that noticed could skip those passes; none here does.

No most-significant-digit variant. This is the least-significant-digit sort, which is the stable one and the one whose pass count is fixed. An MSD radix sort recurses on buckets, stops early on small ones, and has a completely different access pattern; it is not measured.

One cache. Sixty-four lines of eight elements, fully associative, LRU, as everywhere in this collection. The miss counts are a model, and the crossing between four-bit and sixteen-bit digits depends on it — a larger cache holds more streams and moves the bottom of the curve to a wider digit.

Counters counted as words, not as a working set. The 256 counters of an 8-bit radix are four lines and are resident throughout; the 65,536 of a 16-bit radix are a thousand lines and are not. That difference is in the miss counts and not in the space column, which reports words.

The floor is for distinct keys. log2n!\log_2 n! counts orderings of distinguishable things, and the floor when the values repeat measured how far it overstates when they do not. Every key here is distinct, so the comparison of merge sort’s 965,752 against 954,037 is the fair one.

Counters, not a full accounting of space. The 8-bit radix holds 256 counters and an nn-word scratch array, so its peak auxiliary space is n+256n + 256 — the same order as merge sort’s nn buffer, and measuring what an algorithm keeps is the argument that this axis has to be counted rather than assumed. It is counted here and it is not what separates the two.

And nothing is timed. A pass of a radix sort is a tight loop with no branch on data, and a comparison sort’s inner loop is a branch the processor must predict — which the branch the machine guesses measured and which no count on this page reports.

Still open: the sort that chooses its own radix

The digit width on this page is a constant chosen before the sort runs, and the plate against it has a shape — data moved falling, misses rising then falling, counters exploding — that has a best setting for every combination of nn, ww and the cache. Nothing here finds it, and a real implementation has to.

The interesting version is the one that does not have to be told. The number of streams a pass can afford is the cache’s capacity in lines, which a cache-oblivious method may not ask for; but the number of distinct values actually present in a digit is something the counting pass measures for free, and a digit whose counts are concentrated in a few buckets is a digit that could have been wider.

The measurement that follows sweeps nn, the key width and the cache size, finds the digit width minimising total misses at each point, and asks two things: whether that width is predicted by the cache’s capacity in lines — the obvious rule, one stream a line — and whether a sort that widened its digit whenever the previous pass’s counts were concentrated lands near it without being told the cache size at all.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

The objects this essay names

Each one links to every other essay that touches it.

CacheComparison countCounting conventionInformation floorKey widthLower boundMeasurement designModelRadix sortSpace time tradeStability