What the libraries do

The threshold somebody chose

A minrun of 32. An insertion cutoff of 16. A gallop threshold of 7. A depth limit of twice the logarithm. Four numbers, in four real source files, none of which appears in any complexity analysis — and each of which decides more about what these algorithms do than the analysis does. Swept, they turn out not to be optima, and finding out what they are instead is the point.

Every essay on this site until this phase has been concerned with constants the notation throws away. Merge sort and heapsort are both Θ(nlogn)\Theta(n \log n) 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

minrun swept, Timsort at n = 8,192minrun is how long a natural run is extended to before it is pushed, and it is one number in a source file. Swept across 4–256 with everything else held, the comparisons vary as drawn. The shipped value is 32 at this n, marked; the measured minimum is at 256 on random and 4 on nearly sorted, costing 0.5% and 27.1% more than the best available. A threshold chosen against one counter is not chosen against the others, and this figure only shows one of them.1010010⁵minruncomparisonsshipped: 32randomnearly sortedTimsort, n = 8,192comparisons; rings mark the measured minimum
Fig. 1 minrun swept from 4 to 256 on two inputs at n = 8,192, with the shipped value marked. On random input the comparison count is flat to within 7% across the whole sweep. On nearly sorted input it is monotonically increasing, and the best value is the smallest one tried.

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 kk by binary insertion costs Θ(logk)\Theta(\log k) comparisons and Θ(k)\Theta(k) 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 nn, 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 n/minrunn/\text{minrun} 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 n/minrunn/\text{minrun} 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 n=1024n = 1024, 33 at n=1025n = 1025, 63 at n=1000n = 1000, 32 again at n=4096n = 4096.

That is checkable and the gate checks it: for a range of sizes, n/minrunn/\text{minrun} 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 n=8,192n = 8{,}192, where 8192/32=2568192/32 = 256 exactly — a power of two, the best case for the rule. At n=8,193n = 8{,}193 the rule gives 33 and n/minrunn/\text{minrun} 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 nn at fixed minrun would show a sawtooth that this one cannot. That is a real limitation and it is why the figure names its nn.

The insertion cutoff

insertion cutoff swept, Introsort at n = 8,192insertion cutoff is the subarray size below which quicksort stops and insertion sort finishes, and it is one number in a source file. Swept across 1–96 with everything else held, the comparisons vary as drawn. The shipped value is 16, marked; the measured minimum is at 8 on random and 96 on nearly sorted, costing 0.9% and 24.8% more than the best available. A threshold chosen against one counter is not chosen against the others, and this figure only shows one of them.11010⁵insertion cutoffcomparisonsshipped: 16randomnearly sortedIntrosort, n = 8,192comparisons; rings mark the measured minimum
Fig. 2 Introsort’s insertion cutoff swept from 1 to 96 at n = 8,192. The comparison count has a shallow minimum around 8 to 12 and the shipped 16 sits just past it, costing 0.9% more than the best value tried.

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?

insertion cutoff swept, Introsort at n = 8,192insertion cutoff is the subarray size below which quicksort stops and insertion sort finishes, and it is one number in a source file. Swept across 1–96 with everything else held, the branch mispredictions vary as drawn. The shipped value is 16, marked; the measured minimum is at 96 on random and 64 on nearly sorted, costing 25.2% and 91.5% more than the best available. A threshold chosen against one counter is not chosen against the others, and this figure only shows one of them.11010⁴insertion cutoffbranch mispredictionsshipped: 16randomnearly sortedIntrosort, n = 8,192branch mispredictions; rings mark the measured minimum
Fig. 3 The same sweep, counting modelled branch mispredictions instead. This curve falls monotonically: the larger the cutoff, the fewer mispredictions, because insertion sort’s inner loop is nearly always predictable and a partition’s test is nearly a coin flip. On this counter the best value is the largest one tried.

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

depth factor swept, Introsort at n = 8,192depth factor is the multiple of log₂ n at which quicksort gives up and calls heapsort, and it is one number in a source file. Swept across 0.25–6 with everything else held, the comparisons vary as drawn. The shipped value is 2, marked; the measured minimum is at 2 on random and 0.5 on few distinct values, costing 0.0% and 197.4% more than the best available. A threshold chosen against one counter is not chosen against the others, and this figure only shows one of them.110⁵depth factorcomparisonsshipped: 2randomfew distinct valuesIntrosort, n = 8,192comparisons; rings mark the measured minimum
Fig. 4 Introsort’s depth limit swept as a multiple of log₂ n, from a quarter to six. On random input the curve falls and flattens: below about 1.5 the heapsort fallback fires often and costs comparisons, above 3 it never fires at all. On input with few distinct values the curve does the opposite, and it does not flatten.

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 nn, 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.

minrun swept, Timsort at n = 512minrun is how long a natural run is extended to before it is pushed, and it is one number in a source file. Swept across 4–256 with everything else held, the comparisons vary as drawn. The shipped value is 32 at this n, marked; the measured minimum is at 256 on random and 4 on nearly sorted, costing 1.0% and 39.6% more than the best available. A threshold chosen against one counter is not chosen against the others, and this figure only shows one of them.10100minruncomparisonsshipped: 32randomnearly sortedTimsort, n = 512comparisons; rings mark the measured minimum
Fig. 5 minrun swept from 4 to 256 at n = 512 rather than 8,192. The shipped value is 32 here as well; the measured minimum is at 256 on random input and at 4 on nearly sorted, costing 1.0% and 39.6% more than the best available. One constant, two inputs, and the right value for each is at the opposite end of the range.

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 n=8,192n = 8{,}192 is about log2(8192/96)6.4\log_2(8192/96) \approx 6.4 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.

Comparisons on random input, n = 5124 of these are what a standard library actually runs. Timsort is lowest at 3,948 and Insertion highest at 63,379, a factor of 16.05. Changing the counter changes the order, which is the reason this field measures four of them and refuses to name a winner.Timsort3,948shipsIntrosort5,946shipspdqsort4,966shipsDual-pivot4,822shipsInsertion63,379textbookMerge sort3,982textbookHeapsort7,673textbookalgorithmcomparisonsrandom, n = 512comparisons, counted exactly
Fig. 6 The same seven algorithms at n = 512 rather than 8,192 — a size at which the thresholds cover a substantial fraction of the array and the asymptotic ordering has not fully arrived. Insertion sort, which is quadratic, is a factor of sixteen behind Timsort here and a factor of a hundred and seventy-six behind it at n = 8,192.

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 nn, 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 log2n\log_2 n, 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 nn 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.

Natural runs at n = 256, seed 20260811Each strip is one input, split into the ascending and descending stretches Timsort finds before it sorts anything. Light blocks ascend, dark blocks descend. random: 103 · nearly sorted: 21 · already sorted: 1 · few distinct values: 106 · reversed: 1 runs. A descending run costs nothing to fix — it is reversed in place — which is why reversed input is one run here rather than a worst case.each block is one natural runrunsrandom103longest 5 · 16,073 inversionsnearly sorted21longest 64 · 42 inversionsalready sorted1longest 256 · 0 inversionsfew distinct values106longest 5 · 14,043 inversionsreversed1longest 256 · 32,640 inversionsn = 256, seed 20260811light ascends · dark descends
Fig. 7 The five inputs at n = 256, which is where the thresholds bite hardest: an insertion cutoff of 16 covers a sixteenth of this array in one step, and minrun of 32 means Timsort merges at most eight runs whatever the picture above shows. At the sizes most programs actually sort, the thresholds are not a detail of the algorithm — they are most of it.

That last point deserves the final word. Every figure on this site is drawn between n=32n = 32 and n=65,536n = 65{,}536, 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.

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