The threshold somebody chose
Every essay on this site until this phase has been concerned with constants the notation throws away. Merge sort and heapsort are both and their comparison counts differ by a factor of two; that factor is a property of the algorithms, it emerges from their structure, and nobody chose it.
The constants in this essay are different in kind. They were typed into a source file by a person, they can be changed by editing one line, and the algorithm’s behaviour changes when they are. There are four of them worth measuring, they are all in code running on billions of devices, and none of them is documented with a derivation.
| constant | where | shipped value | what it decides |
|---|---|---|---|
minrun |
CPython listobject.c |
32–64, computed from n | how long a run is padded to before merging |
MIN_GALLOP |
CPython listobject.c |
7 | when a merge switches to searching |
| insertion cutoff | libstdc++ stl_algo.h |
16 | when quicksort stops and insertion sort finishes |
| depth factor | libstdc++ stl_algo.h |
2 (× log₂ n) | when quicksort gives up and heapsort takes over |
The site’s discipline says a constant is measured rather than repeated. So: sweep each one, hold everything else, and see what the shipped value bought.
minrun
The comparison count says: use 4. On nearly sorted input, minrun = 4 costs 26,703 comparisons and the shipped 32 costs 33,449 — 25% more. On random input the whole sweep spans 95,270 to 102,191, a 7% band with no clear structure, and the shipped value sits in the middle of it.
So on the counter this site was built around, minrun is a pessimisation. It makes the adaptive case worse and the random case no better. If comparisons were the only quantity, the right value would be 1 — do not pad runs at all, merge whatever the input contains.
This is exactly where the site’s habit earns its keep: do not conclude the library is wrong, ask which counter it was chosen against. There are three others available.
| minrun | comparisons (random) | writes (random) | merges | comparisons (nearly sorted) | writes (nearly sorted) |
|---|---|---|---|---|---|
| 4 | 102,191 | 105,023 | 2,040 | 26,703 | 74,318 |
| 16 | 96,119 | 111,557 | 511 | 29,829 | 76,218 |
| 32 | 95,772 | 137,457 | 255 | 33,449 | 72,174 |
| 64 | 95,458 | 195,073 | 127 | 38,648 | 67,056 |
| 256 | 95,270 | 569,247 | 31 | 49,781 | 49,578 |
There it is. On random input the comparison count is flat and the write count grows by a factor of 5.4 across the sweep, because extending a run to length by binary insertion costs comparisons and moves — logarithmic in the counter that is flat, linear in the counter that is not.
And in the other direction, the number of merges falls from 2,040 to 31. A merge is not free beyond its comparisons: it copies a run into scratch, walks two pointers, and copies back. Two thousand merges of thirty-two elements each is a great deal of loop setup for the same comparisons as thirty-one merges of two thousand.
So minrun sits where the comparison curve has gone flat and the write curve has not yet turned up — somewhere between 16 and 32 at this size, and the shipped rule produces 32. It is not the minimum of anything. It is the knee of a trade between two counters that move in opposite directions, and neither of them alone identifies it.
That is the finding this field was built to produce, and it took two counters to see. One counter says “smaller”; the other says “larger”; the shipped value is in between, and a sweep of either one alone would have concluded the library was wrong.
The rule that computes it, and the reason it is not a constant
minrun is not literally a number in CPython. It is computed from the array’s length by a rule that fits in five lines: take the top six bits of , and add one if any bit below them is set. The result is always between 32 and 64.
The stated reason is that Timsort’s merge tree balances best when the number of runs is a power of two. If comes out at 2,049, the last merge joins a run of 2,048 minruns against a single one, and that merge costs a full pass over the array to move one run’s worth of elements. If it comes out at exactly 2,048, every merge joins equal-sized runs and none of the work is wasted.
The rule arranges for to be just at or below a power of two rather than just above one, which is a different objective from minimising minrun and produces the jagged values it produces: 32 at , 33 at , 63 at , 32 again at .
That is checkable and the gate checks it: for a range of sizes, is required to sit in the upper half of the interval below the next power of two. It is one of the few constants in this essay with a stated derivation, and it is the only one where the derivation turns out to be about the shape of the merge tree rather than about a cost.
Which raises a question the sweep above cannot answer. The measurements were all taken at , where exactly — a power of two, the best case for the rule. At the rule gives 33 and is 248.3, and the merge tree is not balanced. Everything above is therefore measured at the size the rule is happiest at, and a sweep across at fixed minrun would show a sawtooth that this one cannot. That is a real limitation and it is why the figure names its .
The insertion cutoff
The comparison count is minimised at 8, at 133,623, and the shipped 16 costs 134,880 — a difference of 0.9%. That is close enough to be uninteresting on its own, so again: which counter?
The two counters point in opposite directions and the shipped value is between them again:
| cutoff | comparisons | reads | mispredictions |
|---|---|---|---|
| 1 | 143,931 | 362,005 | 44,072 |
| 8 | 133,623 | 314,345 | 41,903 |
| 12 | 133,632 | 309,789 | 40,937 |
| 16 | 134,880 | 309,920 | 40,025 |
| 32 | 146,651 | 327,026 | 36,914 |
| 96 | 209,837 | 446,449 | 31,647 |
Comparisons are minimised at 8. Reads are minimised at 12. Mispredictions fall all the way to 96 and beyond. The shipped 16 is 0.9% off the comparison optimum, 0.04% off the read optimum, and better on branches than either of them.
Sixteen is not the best value for any of the three counters and it is close to the best for two of them while being on the right side of the third. That is what a well-chosen threshold looks like once there are enough counters to see it — and it is worth noticing that this observation was not available to this collection until the branch counter existed — without it the answer would have been “the shipped value is 0.9% wrong”.
MIN_GALLOP
Swept in when galloping pays, where the answer turned out to be that the question is malformed: MIN_GALLOP is not a fixed parameter during a sort. It starts at 7 and the algorithm moves it — down by one every time a gallop pays, up by two every time the mode is left — so the shipped value is an initial condition for a feedback loop rather than a setting.
A sweep that holds it fixed measures a version of the algorithm that does not exist. That figure is drawn anyway, and it is labelled, because a measurement of the wrong thing that says so is more useful than an absence: it shows the cost is flat below 7 and rises above it, which is the reason 7 is a safe starting point in either direction.
The depth factor
This one is the most interesting of the four, because the two inputs disagree completely and the disagreement is not a matter of a few per cent.
| depth factor | random | fallbacks | few distinct values | fallbacks |
|---|---|---|---|---|
| 0.5 | 172,821 | 40 | 97,106 | 7 |
| 1 | 144,995 | 76 | 138,700 | 8 |
| 1.5 | 136,270 | 19 | 187,168 | 8 |
| 2 | 134,880 | 3 | 243,350 | 8 |
| 3 | 134,688 | 0 | 346,648 | 8 |
| 6 | 134,688 | 0 | 648,430 | 8 |
On random input, larger is better and the improvement stops at 3. On few distinct values, smaller is better by a factor of 6.7 between 0.5 and 6, and there is no flattening at all: every extra level of quicksort on duplicate-heavy input is spent splitting a block of equal elements that heapsort would have finished.
The shipped 2 is a compromise that costs 0.14% on random input and a factor of 2.5 on duplicates. Given the previous essay’s argument that duplicates are the common case rather than the special one, that looks like the wrong compromise — and the reason it is not is that introsort’s authors were not choosing between these two columns. They were choosing a value at which quicksort’s guarantee holds and the fallback essentially never runs, because the fallback is heapsort, and heapsort’s constant is worse and its memory behaviour is much worse, and a program whose sort silently becomes heapsort on a fifth of its inputs has a performance profile nobody can reason about.
The duplicate problem is real and introsort’s answer to it is not the depth limit. It has no answer to it, which is why pdqsort exists.
The other thing every sweep above holds fixed is , and a threshold is a constant measured against a size. Run the first sweep at a sixteenth of the input and the shipped value stops being anywhere near the minimum.
The thresholds interact, which is why none of them can be tuned alone
Every sweep above holds the other three constants fixed. That is the only way to draw a two-dimensional figure and it hides something.
The insertion cutoff and the depth factor are not independent: a larger cutoff means fewer partition levels, which means the recursion is shallower, which means the depth limit is reached less often. At a cutoff of 96 the recursion at is about levels deep before the cutoff catches it, against a limit of 26 — so the depth factor could be halved with no effect at all, and the sweep of the depth factor at cutoff 16 says nothing about its behaviour at cutoff 96.
Timsort’s are worse entangled. minrun decides how many runs there are; the number of runs decides how many merges the policy performs; the merges are where galloping fires; and galloping’s threshold moves during the sort in response to how well it is doing. Four constants, three of them affecting the conditions under which the fourth is evaluated.
The honest description of what the four figures above show is: the cost as one constant varies with the others at their shipped values. That is a one-dimensional slice through a four-dimensional surface, taken at the point somebody else chose, and it can only ever answer “was this choice locally reasonable” rather than “is this the best choice”. Both figures say so.
It is also the reason these constants have not moved in twenty years despite the hardware changing completely underneath them. A constant that could be tuned independently would have been retuned; a constant entangled with three others, in code that billions of programs depend on behaving predictably, is left alone unless something is demonstrably wrong. The stability of these numbers is a fact about the risk of changing them rather than evidence that they are optimal, and the sweeps above are the closest this site can come to saying which.
Three of the four are absolute and one is a ratio
Set the four constants beside each other and they divide into two kinds, and the division decides how each of them ages.
minrun is between 32 and 64. MIN_GALLOP is 7. The insertion cutoff is 16. All three are absolute counts of elements, and none of them mentions , the word size, the cache line or anything else about the machine.
The depth factor is 2 × log₂ n. It is a ratio, it scales with the problem, and it means the same thing at every size.
That difference matters because of what the absolute constants are implicitly relative to. An insertion cutoff of 16 is a claim that insertion sort wins below about sixteen elements, and that claim is a fact about how a cache line, a register file and a branch predictor behave — none of which are in the source, all of which have changed by more than a factor of sixteen since the number was chosen. A threshold expressed as a bare integer is a threshold that has been silently detached from the quantity it was really about.
The measurements in this essay show what that detachment looks like from the inside. The insertion cutoff’s comparison optimum is 8, its read optimum is 12, and its branch optimum is off the top of the sweep — three different numbers, all in units of elements, and the shipped 16 is a settlement between them under one set of relative costs. Change the relative cost of a mispredicted branch against a comparison, which is exactly what a decade of processor design does, and the settlement moves without a line of the source changing.
The depth factor has no such exposure. It is defined against , and what it is really about — how many levels of quicksort are permitted before the recursion is judged pathological — is a property of the algorithm rather than of the hardware. It is also, of the four, the one whose sweep above has a genuinely flat region on the input it was chosen for: anything from 2 to 6 is identical on random data.
So one of the four is scale-free and three of them are calibrated against a machine that no longer exists. That is not an argument that they are wrong — the sweeps say the insertion cutoff is well placed even now — but it says which of them would be worth re-measuring first, and it says why the answer would be a different number rather than the same one.
Why they are not tuned at run time
The obvious repair to a constant that has drifted is to stop having a constant: measure at run time and choose. It is worth saying why no library sort does this, because the reasons are good ones and they are not laziness.
The measurement costs more than the win. The insertion cutoff’s optimum spans 8 to 16 and the difference across that span is about one per cent of comparisons. Detecting which end of it a particular machine is at requires timing runs whose noise is larger than the effect, and a sort that spends a millisecond calibrating before sorting two hundred elements has lost by orders of magnitude.
It destroys reproducibility. A sort whose behaviour depends on a measurement taken at start-up is a sort that does different things on the same input on the same machine on different days. For a library that everything depends on, a performance profile that cannot be reproduced is worse than one that is one per cent off.
And the thresholds interact, as the section above sets out, so tuning one at run time means tuning a four-dimensional surface at run time — with each dimension’s optimum depending on where the others sit.
What is done instead is tuning at build time, by the people who ship the library, on the hardware of the day, once. That is the correct answer to all three objections and it explains the stability the previous section describes: the constants have not moved because moving them requires somebody to redo the four-dimensional measurement and defend the result against every workload in the world.
What a threshold is
Four constants, four sweeps, and the same result four times: the shipped value minimises nothing.
That could be read as sloppiness and it is the opposite. A constant that minimised one counter would be a constant chosen by someone who measured one thing. Each of these sits at a point where several quantities have been traded against each other, and the trade is invisible without all of them. This site now has five, and it took the fifth to see that the insertion cutoff of 16 is well chosen rather than 0.9% wrong.
There is a second consequence and it is the more useful one. These constants are the algorithm. Timsort with minrun set to 1 and MIN_GALLOP set to infinity is a bottom-up merge sort with run detection; set minrun to and it is binary insertion sort. The same code, the same complexity class, and two completely different algorithms, distinguished by numbers that appear nowhere in the analysis.
The count somebody chose argued that choosing what to count decides what an analysis can conclude. This is the same argument one level in: choosing the thresholds decides what the algorithm does, and an analysis conducted at the level of asymptotic classes cannot see any of it.
That last point deserves the final word. Every figure on this site is drawn between and , and the fits are stated over ranges starting at 256 for exactly the reason this essay has been circling. Real programs sort small arrays constantly — a few dozen elements, a few hundred — and in that range a library sort is almost entirely its thresholds. The asymptotic class describes what happens somewhere most sorting never reaches.
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.
- One run, four counts, four answers comparison count · cutoff · quicksort
- The branch the machine guesses comparison count · misprediction · timsort
- The pattern that defeats the pattern depth limit · introsort · quicksort
- Where insertion sort actually wins comparison count · cutoff · threshold
- A count over every input comparison count · quicksort
- A distribution computed rather than sampled comparison count · quicksort
What links here
The 8 essays that link to this one and share the most of its objects, of 30 that link here.
The objects this essay names
Each one links to every other essay that touches it.
Comparison countCutoffDepth limitGallopingInsertion cutoffIntrosortMIN_GALLOPminrunMispredictionQuicksortThresholdTimsortTuning constant