Counting

The exchange rate nobody wrote down

Three earlier essays have said in passing that the ranking would change if the elements were large records. None of them computed it. Computed, selection sort goes from second-worst of seven at four bytes a record to best of seven at five hundred and twelve — and the crossover against each rival is a division that takes one line.

This collection has made the same promise three times and never kept it.

“If the elements being sorted are machine integers, selection sort’s write economy is worth very little. If they are large records that must be physically moved, it is worth a great deal, and the ranking by comparisons is the wrong ranking entirely.”counting instead of timing.

“If a record is 200 bytes and a key is 4, a write is fifty times a comparison and the write column is the one that matters.”one run, four counts, four answers.

“Sorting 200-byte strings, a single comparison walks up to 200 bytes, and the comparison count is then a count of variable-cost operations dressed up as a count of atoms.”what O notation does not say.

Three statements, all correct, none of them a number. This page is the number.

Quicksort is cheapest at 4 bytes a record and Selection sort at 512The same 7 runs at n = 512, ranked by bytes moved under four record sizes. Nothing about the runs changes down the plate: the comparisons, reads and writes are the same numbers in every column, and only the exchange rate between them moves. At four bytes a record the ranking is the familiar one. At 512 the algorithm that makes more comparisons than any other here is the cheapest of them, because it writes 1,008 elements against 4,760. There are 3 places in this sweep where the order changes.4 bytes32 bytes128 bytes512 bytesrecord:QuicksortMerge sortShellsortHeapsortInsertion sortSelection sortBubble sortQuicksortShellsortMerge sortHeapsortSelection sortInsertion sortBubble sortQuicksortShellsortMerge sortSelection sortHeapsortInsertion sortBubble sortSelection sortQuicksortShellsortMerge sortHeapsortInsertion sortBubble sort1234567n = 512, random input, key 8 bytesa read or a write moves the record; a comparison touches the key
Fig. 1 Seven sorting algorithms at 512 elements, ranked by bytes moved under four record sizes. Nothing about the runs changes down the plate: the comparisons, reads and writes are the same integers in every column and only the exchange rate between them moves. At four bytes a record the ranking is the familiar one. At five hundred and twelve, the algorithm that makes more comparisons than any other here is the cheapest of them, because it writes 1,008 elements against everything else’s thousands. The order changes at every step of the sweep.

It is worth asking why a promise gets made three times and kept none. The answer is not laziness; it is that keeping it requires a decision the essays were each in the middle of avoiding. Combining two counts into one number needs an exchange rate between them, and an exchange rate is a claim about somebody’s hardware and somebody’s data — exactly the kind of claim this collection is careful not to make. So each essay noted the effect, declined the arithmetic, and moved on. What follows is the arithmetic done with the rate stated as a parameter rather than assumed, which is the form the earlier essays could have used and did not.

The model, stated so it can be argued with

A cost in bytes needs a rule and the rule is a choice. This one:

A read or a write moves the whole record. A comparison touches the key only.

So for a run with cc comparisons, rr reads and ww writes, the bytes are (w+r2c)record+ckey(w + r - 2c)\cdot\text{record} + c\cdot\text{key} — the 2c2c subtracted because this site’s counter charges a comparison as two reads, and those two reads are reads of keys rather than of records.

That subtraction is not a detail. The first version of this arithmetic charged r+wr + w at the record rate and reported selection sort as the heaviest mover on the site, which is the exact opposite of the effect the model exists to show: selection sort makes 130,816 comparisons at this size, each counted as two reads, and charging those at 200 bytes apiece buries everything else. An implementation sorting large records compares keys and moves records, and the two rates have to be applied to the two different things.

With the subtraction, the quantity that matters becomes visible:

comparisons elements moved
selection 130,816 2,016
quicksort, median of 3 5,049 5,659
shell 5,840 6,336
merge 3,964 9,216
heap 7,653 16,680
insertion 63,071 63,077
bubble 129,688 250,252

Selection sort moves 2,016 elements. Nothing else on the site moves fewer, and it is the only algorithm here whose movement is O(n)O(n) rather than O(nlogn)O(n \log n) or worse. That is its entire and only virtue, it has been visible in this collection’s counts since the first plate, and until now nothing has said what it is worth.

There is a general point in that first-version failure and it is worth extracting, because it is a mistake this collection is well placed to make repeatedly. A counting convention chosen for one purpose becomes a modelling assumption when the counts are combined. Charging a comparison as two reads is a perfectly good convention for measuring memory traffic in an integer sort — the reads are real, and an implementation that cached one operand would perform fewer, which the convention says openly. It stops being a convention and becomes a claim the moment those reads are priced at a record’s width, because in the world where a record is 200 bytes an implementation would not be reading records to compare them.

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. 2 The pair of counts this site opened with, on the same run: comparisons against swaps. Selection sort has the most of the first and among the fewest of the second, and the essay that drew this plate concluded that the question “how many operations” has no answer until the operation is named. The plate above is what happens when the two are combined at a stated exchange rate instead of being left as two columns.

Where each pair changes places

Bytes are linear in the record size, so two algorithms cross at most once and the crossing is one division. If mm is elements moved and cc is comparisons, algorithm A beats B exactly when

(mAmB)record<(cBcA)key(m_A - m_B)\cdot\text{record} < (c_B - c_A)\cdot\text{key}

which is a single value of the record size, positive or nowhere. Against selection sort at 512 elements and an eight-byte key:

against crosses at
bubble 0.04 bytes
insertion 8.9 bytes
heapsort 67 bytes
merge sort 141 bytes
shell sort 231 bytes
quicksort, median of 3 276 bytes

Read that column downwards and it is a schedule. Below nine bytes a record, selection sort is beaten by insertion sort. Above 276 bytes, it beats everything measured here. Between those, it overtakes its rivals one at a time in an order that has nothing to do with their complexity classes — heapsort first, then merge sort, then two algorithms that beat both of them at every size.

“It depends on the record size” is usually said as though the answer were a range. It is a point, and the point is computable from counts the site already had.

Quicksort is cheapest at 4 bytes a record and Quicksort at 256The same 7 runs at n = 512, ranked by bytes moved under four record sizes. Nothing about the runs changes down the plate: the comparisons, reads and writes are the same numbers in every column, and only the exchange rate between them moves. At four bytes a record the ranking is the familiar one. At 256 the algorithm that makes more comparisons than any other here is the cheapest of them, because it writes 4,760 elements against 4,760. There are 3 places in this sweep where the order changes.4 bytes8 bytes16 bytes64 bytes256 bytesrecord:QuicksortMerge sortShellsortHeapsortInsertion sortSelection sortBubble sortQuicksortShellsortMerge sortHeapsortInsertion sortSelection sortBubble sortQuicksortShellsortMerge sortHeapsortSelection sortInsertion sortBubble sortQuicksortShellsortMerge sortHeapsortSelection sortInsertion sortBubble sortQuicksortSelection sortShellsortMerge sortHeapsortInsertion sortBubble sort1234567n = 512, random input, key 8 bytesa read or a write moves the record; a comparison touches the key
Fig. 3 The same seven at five record sizes chosen more closely, where the reordering happens one pair at a time rather than in jumps. Selection sort rises through the table as the record grows, passing heapsort between sixty-four and two hundred and fifty-six, and quicksort’s variants stay at the top throughout this range because their crossing is beyond it.

The crossing points also say something about which algorithms are worth having in a library. Quicksort’s median-of-three variant and shell sort sit at the top of the table for every record size drawn, so nothing in this range dislodges them, and their crossings against selection sort are beyond the largest record anybody sorts by value. Merge sort and heapsort are dislodged at 141 and 67 bytes, which are ordinary record sizes — a struct with a handful of fields. The two algorithms this collection uses as its well-behaved reference points are the two whose ranking is least robust to the exchange rate, which is not a fact anybody would guess from their comparison counts.

What the model gets wrong, in the one place it can be checked

The rule charges a comparison a fixed number of bytes. For a machine integer that is right. For a string it is not, and the error is not a constant.

Comparing two strings walks them until they differ, so the cost is the length of their common prefix plus one. How long that is depends on how many keys share a prefix, and how many keys share a prefix depends on how many keys there are.

That is measurable directly. Sort keys of a fixed length with a comparator that reports how far it had to walk, and sweep the number of keys while holding the length constant:

keys characters examined per comparison
64 4.5
256 5.6
1,024 6.5
4,096 7.4
16,384 8.3

The key length is twenty characters at every row. The only thing changing is how many of them there are, and the cost of a comparison rises by 83% over a 256-fold range — which is a logarithm, and which means the byte cost of a comparison sort on string keys is not in the same class as its comparison count. Θ(nlogn)\Theta(n\log n) comparisons at Θ(logn)\Theta(\log n) characters each is Θ(nlog2n)\Theta(n\log^2 n) characters.

Two things about that table are worth separating, because only one of them is surprising. That a string comparison costs more than an integer comparison is obvious and was already stated three essays ago. That its cost grows with the number of keys is not, and it is what breaks the model rather than merely qualifying it: a fixed rate can be wrong and still be a rate, and a rate that is a function of nn is not a rate at all. It is a term, and it belongs in the class rather than in the constant.

A comparison is not a constant number of bytes: 4.5 characters at n = 64, 8.3 at 16,384Keys of exactly 20 characters over an alphabet of 2, sorted by a comparator that reports how far it had to walk before the two strings differed. The key length is fixed, so the only thing changing along the axis is how many keys there are — and more keys means more of them sharing a prefix, so the average comparison examines more characters. It grows like the logarithm of n, which means the cost of this sort in BYTES is not in the same class as its cost in comparisons.02468642561,0244,09616,384keys sortedcharacters examined per comparisonMerge sort, keys of 20 characters over 2 letters1,733,288 characters over 208,666 comparisons at the right
Fig. 4 The measurement, on keys of exactly twenty characters over a two-letter alphabet. The rise is slow and it is unmistakable, and the reason it can be attributed to the number of keys rather than to their length is that the length is held fixed — a sweep that let the words grow with nn would be measuring two things at once and reporting their sum.
A comparison is not a constant number of bytes: 1.9 characters at n = 64, 2.9 at 16,384Keys of exactly 12 characters over an alphabet of 10, sorted by a comparator that reports how far it had to walk before the two strings differed. The key length is fixed, so the only thing changing along the axis is how many keys there are — and more keys means more of them sharing a prefix, so the average comparison examines more characters. It grows like the logarithm of n, which means the cost of this sort in BYTES is not in the same class as its cost in comparisons.02642561,0244,09616,384keys sortedcharacters examined per comparisonMerge sort, keys of 12 characters over 10 letters604,860 characters over 208,524 comparisons at the right
Fig. 5 The same measurement over a ten-letter alphabet and shorter keys, where two random strings diverge almost immediately: 1.9 characters per comparison at sixty-four keys and 2.9 at sixteen thousand. The growth is the same shape and the constant is far smaller, because how much prefix two keys share is a function of the alphabet as well as of how many there are. A comparison over a large alphabet is nearly free and a comparison over a small one is not, which is the same alphabet-dependence the sparse table turns on in a different field.

Why the exchange rate is a modelling decision and not a measurement

The bytes on the hero plate are computed rather than counted, and it is worth being precise about what that means for their status.

This site’s counters are exact. Comparisons, reads, writes, cache misses, random bits, block transfers — each is an integer produced by an instrumented run, identical on every machine. A byte count is those integers multiplied by two constants somebody chose, and the choosing is where the content is.

That puts it in the same category as the cache model’s line size, the external-memory model’s BB and MM, and the weighting that two pivots and what they cost found inside a published analysis — a parameter that must be stated before the conclusion means anything. The plate states both parameters, and this is why.

The useful thing a stated rate buys is not a number. It is that the conclusion can be recomputed at somebody else’s rate. A reader whose records are forty bytes and whose keys are two can take the two columns of counts above and get their own crossings, from a division. A reader given only “selection sort is good for large records” has to take it or leave it.

What a real system does with this, which is not what the plate suggests

The measurement says selection sort wins above 276 bytes a record. No production sorting routine has ever chosen selection sort for large records, and understanding why is more useful than the crossing.

The reason is that the choice on the plate is a choice between sorting the records where they lie, and nobody does that. The standard move is to sort an array of pointers — or of key-and-index pairs — and permute the records once at the end. Then every algorithm’s movement cost falls to the pointer width, the comparison cost stays whatever the key comparison costs, and the ranking snaps back to the four-byte column at the top of the plate for every record size.

So the honest reading is that the plate measures a regime that a competent implementation escapes, and the escape has a price: an extra array of nn pointers, an extra indirection on every comparison — which is a scattered memory access, and therefore expensive in a unit where an algorithm looks measures rather than in this one — and a final permutation whose cost the external-memory field has already priced.

That does not make the exchange rate uninteresting. It relocates what it decides. The record size does not choose between sorting algorithms; it chooses whether to sort the records at all, and the crossing points on this page are where that second decision turns. A reader whose records are twelve bytes should sort them directly and a reader whose records are five hundred should sort pointers, and the arithmetic that says so is the same arithmetic.

Where the model breaks that this page does not measure

Three ways, and the second is the largest.

A record move is not proportional to its size. Moving 512 bytes is not 128 times moving four, because a machine moves memory in blocks and a large contiguous copy runs at a rate a small one cannot reach. The model’s linearity is the assumption that makes the crossings computable, and it is the assumption that is least true at the top of the range.

Sorting pointers is the real alternative and it is not on the plate. Nobody sorts 512-byte records by moving them; they sort an array of pointers or indices and permute the records once at the end. That changes every number here — the movement becomes eight bytes an element and the permutation at the end is a separate cost, which is exactly the permutation problem the external-memory field measures. The whole reason large-record sorting is interesting is a technique this page’s model cannot express.

And the comparison’s cost has a second dependence. The characters-per-comparison measurement above is on random strings over a two-letter alphabet. Real keys share prefixes for structural reasons — file paths, URLs, sorted identifiers — and the sharing is far heavier than randomness gives. The direction is against the comparison-heavy algorithms and the magnitude is unmeasured.

Insertion sort is cheapest at 4 bytes a record and Bubble sort at 512The same 7 runs at n = 512, ranked by bytes moved under four record sizes. Nothing about the runs changes down the plate: the comparisons, reads and writes are the same numbers in every column, and only the exchange rate between them moves. At four bytes a record the ranking is the familiar one. At 512 the algorithm that makes more comparisons than any other here is the cheapest of them, because it writes 126 elements against 574. There are 3 places in this sweep where the order changes.4 bytes32 bytes128 bytes512 bytesrecord:Insertion sortBubble sortShellsortQuicksortMerge sortHeapsortSelection sortInsertion sortBubble sortQuicksortShellsortMerge sortHeapsortSelection sortBubble sortInsertion sortQuicksortShellsortSelection sortMerge sortHeapsortBubble sortInsertion sortQuicksortSelection sortShellsortMerge sortHeapsort1234567n = 512, nearly sorted input, key 8 bytesa read or a write moves the record; a comparison touches the key
Fig. 6 The same exchange rate applied to nearly sorted input, where the ranking is different at every record size and different again from the random case. Insertion sort is cheapest at four bytes because it does almost no work on nearly sorted input, and bubble sort’s early exit makes it cheapest at five hundred and twelve. The point is not which algorithm wins; it is that the answer needs an input kind as well as a record size, so the ranking is a function of two things neither of which is in any complexity class.

The one algorithm the exchange rate was invented for

Selection sort is the case this page keeps returning to, and it is worth saying exactly what makes it the interesting one rather than merely the extreme one.

Its movement is n1n-1 swaps — at most one per position, and only when the position is wrong. That is not a small constant on an nlognn\log n quantity; it is a different quantity, linear where every other algorithm here is linearithmic or quadratic. So the ratio between selection sort’s movement and anything else’s grows with nn, and the record size at which it overtakes a rival falls as nn rises.

That is a genuine asymptotic statement rather than a fact about 512 elements, and it is the strongest form of the argument this page makes: at a large enough nn, selection sort beats merge sort in bytes moved at any record size larger than the key. What stops that being a recommendation is the comparison column, which is quadratic and which the exchange rate is multiplying by a constant — so the crossing exists and the record size at which it happens is falling, while the absolute cost of both is rising quadratically for one of them.

Working the two against each other gives the honest conclusion. Selection sort’s advantage is real, it is in one column, and it is bought with a column whose growth is worse. An algorithm can be optimal in one resource and inadmissible overall, and this is the site’s clearest example — which is why the collection has reported its two columns separately for as long as it has had them.

What this adds to the count somebody chose

The count somebody chose measured six quantities for every sort, ranked the algorithms by each, and found that comparisons and peak space disagree about 91% of all pairs. Its conclusion was that there is no ranking of sorting algorithms, there are six, and choosing between them is a statement about the data.

This page is the next step and it is a slightly uncomfortable one. The six can be combined, and the combination is a ranking — of a machine. Given a record size and a key size, the seven algorithms here have a total order, and it is computable from the counts already taken. The disagreement between counters does not go away; it is resolved by a parameter, and the parameter is a property of the elements rather than of the algorithms.

Two things follow that are worth separating.

A ranking exists once the exchange rate is fixed, which is more than the earlier essay concluded and is not a contradiction of it: that essay’s point was that no ranking exists without one, and this page supplies one and gets an ordering.

And the exchange rate is not a fact. It is two numbers about the caller’s data, and a paper, a benchmark or a recommendation that does not carry them has fixed them silently. That is the same complaint the earlier essay makes about a weighted combination inside a published analysis, and it is the reason this page prints its rule at the top rather than at the bottom.

Where this ladder goes from here: the count that came from the engine

Every count on this site is described as exact, reproducible to the last digit, and identical on every machine that has ever built the page. That claim is the foundation of the whole collection and it is worth auditing rather than repeating.

It is not quite true, and the leak is in a place nobody would look. Several measurements here call the language’s own sorting routine — an edge sort inside a spanning-tree algorithm, the run-formation pass of an external merge sort — and count the comparisons it makes through the comparator. The standard requires the result to be sorted and stable, and says nothing whatever about how, so those numbers are facts about the engine executing the build rather than about sorting.

The interesting part is not the leak but what a measurement of it shows: that routine is adaptive in a way nothing in its specification mentions, its comparison count on random input comes within about one per cent of the information floor, and it varies by a factor of ten across input kinds where a fixed merge sort varies by a factor of two. All of that is measurable through the comparator, none of it is guaranteed, and every number on this site that passes through it inherits the whole of it.

What this makes readable

Essays that name this one as a prerequisite.

Named alongside this one

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

What links here

Every essay whose body links to this one.

The objects this essay names

Each one links to every other essay that touches it.

Comparison countCost modelCrossoverData movementMeasured countOperation countRankingRecord sizeSelection sortString comparisonTrade offUnit of cost