What the machine does

Where insertion sort actually wins

Every production sorting routine falls back to insertion sort on small subarrays, and the usual explanation is that below some threshold it does fewer comparisons. Measured, it does not — not at sixteen elements, not at eight, not at four. The crossover is real and it is entirely in memory traffic, which is a distinction the usual telling loses.

Open the sorting routine of any standard library and there is a threshold in it. Below some number of elements — 16 in several implementations, 32 in others, 12 in a few — the clever algorithm stops recursing and hands the subarray to insertion sort.

The explanation given is usually some version of: for small n, the constant factors dominate and insertion sort’s are smaller. That is true and it is vague, and the vague version is compatible with a specific claim that people frequently make, which is that below the threshold insertion sort does fewer comparisons.

It does not. Not at sixteen elements, not at eight, not at four.

Where insertion sort actually wins — and it is not in the comparisonsMean over 60 random inputs at each size, both counts on one pair of axes. Insertion sort performs more comparisons than Merge sort at every size measured, including n = 4: the dashed pair never cross. The solid pair — total reads and writes — do cross, between n = 12 and n = 16. The familiar advice to fall back to insertion sort on small subarrays is right, and the reason is memory traffic rather than comparisons, which is a distinction the usual telling of it loses.481632641281010010³10⁴noperations (mean of 60 runs)Insertion trafficMerge trafficInsertion cmpMerge cmptraffic crossessolid: reads + writes · dashed: comparisonstraffic crosses between n = 12 and 16; comparisons never do
Fig. 1 Insertion sort against merge sort, both counts on one pair of axes, averaged over sixty random inputs at each size. The dashed lines are comparisons and they never cross — insertion sort makes more at every size measured, including n = 4. The solid lines are total reads and writes, and those do cross, between n = 12 and n = 16.

The measurement

Mean over sixty random inputs at each size:

n insertion cmp merge cmp insertion traffic merge traffic
4 5.0 4.7 17.2 25.4
6 11.6 10.1 37.8 52.2
8 19.1 15.8 61.0 79.5
12 42.5 30.4 132.3 148.8
16 74.5 45.6 228.8 219.2
24 161.4 82.2 490.5 388.4
32 278.2 121.1 841.5 562.3
64 1,073.0 304.8 3,227.3 1,377.5

“Traffic” is reads plus writes — every element fetched and every element stored.

The comparison columns tell an unambiguous story: merge sort is ahead at four elements and pulls further ahead at every size after that. There is no crossover to find, and a figure looking for one would find nothing.

The traffic columns tell a different story. Insertion sort is ahead at 4, 6, 8 and 12; merge sort is ahead at 16 and everything above. The crossover is between 12 and 16 — which is, satisfyingly, exactly the range where real implementations put their thresholds.

The crossover is a claim about a mean, and the mean hides a band

Every number in that table is an average over sixty inputs, and averaging is a choice that this site is elsewhere quite rude about. It is worth turning the same rudeness on the headline result.

Counting instead how many of the sixty individual inputs insertion sort wins on traffic:

n insertion wins median gap
4 60 of 60 −9
8 55 of 60 −17
12 44 of 60 −16
16 30 of 60 +7
20 10 of 60 +41
24 4 of 60 +100
32 0 of 60 +281

A negative gap means insertion sort moved less data on the median input.

The mean crosses between 12 and 16. The vote crosses at 16 and it crosses by the narrowest margin available — thirty inputs each way, an exact tie. And the last input on which insertion sort wins is somewhere between 24 and 32: at n = 24 there are still four random arrays out of sixty that it handles with less traffic than merge sort, and only at 32 does merge sort win all sixty.

So the crossover is not a point. It is a band about twelve to thirty-two elements wide, inside which which algorithm is cheaper depends on the particular array, and the threshold a library picks is a decision about where inside that band to stand rather than a discovery of where the lines meet.

That matters for the folklore in a second way. A cutoff of 16 is not “the point at which insertion sort stops winning”; it is the point at which it stops winning more often than not. An implementation with a cutoff of 24 is still winning on one subarray in fifteen, and an implementation with a cutoff of 12 is giving up wins on more than two subarrays in three. Both are defensible, which is why both exist.

The width of the band is also why nobody has settled the argument by measurement. Sixty inputs put the tie at 16; a different generator, a different element distribution, or a different number of trials will move it by a few elements, and the three effects below that the traffic count does not see move it further. A band is the honest shape of this answer and a threshold is the shape an implementation needs, so somebody has to round.

Why the two counts disagree

Merge sort is extremely good at comparing and comparatively expensive at moving.

Its comparison efficiency is near the theoretical floor: each comparison in a merge determines exactly one output element and is never repeated. Even at n = 4 that efficiency is already showing.

Its traffic is another matter. A merge reads from the source array, writes into a buffer, and then copies the buffer back. Every element is therefore read at least twice and written at least twice per level of recursion, plus the reads charged by the comparisons themselves. At n = 4 that is 25.4 accesses to place four elements.

Insertion sort’s traffic at n = 4 is 17.2. It reads an element, walks backwards shifting until it finds the place, and writes it down. On a nearly-sorted or tiny array the backwards walk is short and the total movement is small. It does more comparing and less carrying.

So which algorithm is cheaper depends on which of those is being charged for, and at small nn the two counts give opposite answers. This is the site’s recurring theme in its sharpest form: the counts are exact, they disagree, and the disagreement is not noise.

Why the comparison count was never going to cross

It is worth seeing why merge sort’s comparison advantage holds even at four elements, because the reason is not an accident of the implementation.

Merge sort at n=4n = 4 splits into two pairs, sorts each with one comparison, and merges with at most three — a maximum of five comparisons, averaging 4.7. The information-theoretic floor for four elements is log224=5\lceil\log_2 24\rceil = 5 in the worst case, so merge sort is at the floor here. It cannot be beaten.

Insertion sort at n=4n = 4 averages 5.0. It is above the floor even at this size, because its inner loop rediscovers the position of each element by walking, and the walking repeats comparisons that a merge would have made once.

So the comparison crossover does not exist because merge sort is optimal at every size in this range and insertion sort is not. There is no size small enough for insertion sort to catch up, and there never was going to be.

What insertion sort has instead is a small constant per operation — no recursion, no buffer, no bookkeeping — and that constant lives in the traffic count and in things below the traffic count. The folklore compressed “smaller constants” into “fewer comparisons”, and the two are not the same claim.

What the real reason is

The traffic count is a better proxy for what hardware charges than the comparison count, and there are three further effects it still does not capture — all of which point the same way.

Recursion has a cost that no array count sees. Merge sort at n = 4 makes several function calls. Each is a stack frame, a few register saves, a branch the processor may not predict. Insertion sort at n = 4 is two nested loops with no calls at all. The instrumented array records nothing about this because nothing touches the array.

The buffer competes for cache. Merge sort’s scratch space is the size of the input. On a small subarray this is trivial, but the buffer is allocated once for the whole sort, so it is a second array of the full size being touched alongside the first, halving the effective cache available at every level.

Branch prediction. Insertion sort’s inner loop on a nearly-sorted subarray exits after one or two iterations, predictably. Merge sort’s inner comparison is essentially a coin flip on random data, and a mispredicted branch costs on the order of fifteen cycles — comparable to a cache miss, and equally invisible in every count on this site.

All three make insertion sort look better than the traffic count already shows, which means the true crossover is at least where the traffic count puts it and probably a little higher. That is consistent with implementations choosing 16 or 32 rather than 12.

What this site can and cannot say about it

It can say, with exact and reproducible numbers, that the comparison count does not cross. That claim is a refutation of a specific and commonly repeated explanation, and refutation is what finite measurement is good for.

It can say that the traffic count does cross, between 12 and 16, and that this is the count that behaves the way the folklore describes.

It cannot say where the crossover is in time on any particular processor, because this site does not time anything. The three effects above are real and unmeasured here, and each of them moves the threshold. Anyone choosing a cutoff for a real implementation should measure it on the target hardware with the target element type, and the reason to do so is that the answer depends on both.

The figure’s assertions encode exactly this. The generator requires that no comparison crossover exists and that a traffic crossover does. If a future change to either algorithm produced a comparison crossover, the caption would be wrong and the build would stop rather than quietly print a false sentence.

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 same disagreement at a larger size, across more algorithms. At n = 512 the comparison ranking and the swap ranking differ, and both differ from the write ranking. A hybrid algorithm is a decision about which of these to optimise at which scale, and the decision only makes sense once the count has been named.

What the hybrid costs

Since the cutoff makes merge sort worse at comparing, it is worth asking how much.

Plain merge sort’s fitted comparison constant on random input is 0.838. With the cutoff it is 1.059 — a 26% increase, permanently, at every size. The hybrid also does 6.8% more writing.

So the cutoff is a straightforward loss in both counts this site measures well, and it is universally implemented anyway, on the strength of the effects it does not measure. That is a slightly uncomfortable conclusion for a site built on measurement, and it is the same honest limit that the fitting machinery runs into elsewhere: the practitioners are right and the justification is in a quantity the instrument does not reach.

What the instrument does establish is that the justification is not the one usually given. That is a smaller result than settling the question, and it is a real one.

The crossing, three more ways

The crossing is what the essay is about, so it is worth taking on two more inputs and at more trials before a threshold is read off it.

Where insertion sort actually wins — and it is not in the comparisonsMean over 60 random inputs at each size, both counts on one pair of axes. Insertion sort performs more comparisons than Merge sort at every size measured, including n = 4: the dashed pair never cross. The solid pair — total reads and writes — do cross, between n = 5 and n = 6. The familiar advice to fall back to insertion sort on small subarrays is right, and the reason is memory traffic rather than comparisons, which is a distinction the usual telling of it loses.481632641281010010³10⁴noperations (mean of 60 runs)Insertion trafficMerge trafficInsertion cmpMerge cmptraffic crossessolid: reads + writes · dashed: comparisonstraffic crosses between n = 5 and 6; comparisons never do
Fig. 3 Reversed input, which is insertion sort’s worst case in comparisons — and the crossing is still not in the comparisons, because that is not where it ever was.

Few distinct values is the other input worth trying, because a comparison there is much likelier to be an equality and the two algorithms treat that differently.

Where insertion sort actually wins — and it is not in the comparisonsMean over 60 random inputs at each size, both counts on one pair of axes. Insertion sort performs more comparisons than Merge sort at every size measured, including n = 4: the dashed pair never cross. The solid pair — total reads and writes — do cross, between n = 16 and n = 20. The familiar advice to fall back to insertion sort on small subarrays is right, and the reason is memory traffic rather than comparisons, which is a distinction the usual telling of it loses.481632641281010010³10⁴noperations (mean of 60 runs)Insertion trafficMerge trafficInsertion cmpMerge cmptraffic crossessolid: reads + writes · dashed: comparisonstraffic crosses between n = 16 and 20; comparisons never do
Fig. 4 Input with few distinct values, where a comparison is much likelier to be an equality and the two algorithms react to that differently.
Where insertion sort actually wins — and it is not in the comparisonsMean over 200 random inputs at each size, both counts on one pair of axes. Insertion sort performs more comparisons than Merge sort at every size measured, including n = 4: the dashed pair never cross. The solid pair — total reads and writes — do cross, between n = 12 and n = 16. The familiar advice to fall back to insertion sort on small subarrays is right, and the reason is memory traffic rather than comparisons, which is a distinction the usual telling of it loses.481632641281010010³10⁴noperations (mean of 200 runs)Insertion trafficMerge trafficInsertion cmpMerge cmptraffic crossessolid: reads + writes · dashed: comparisonstraffic crosses between n = 12 and 16; comparisons never do
Fig. 5 And the original input over two hundred trials rather than sixty, which is the check that the crossing is a threshold rather than an artefact of how many runs went into the mean. It does not move.

The last of the four holds everything fixed but the count of runs again, in the other direction, so that the pair brackets the setting the page actually quotes.

Where insertion sort actually wins — and it is not in the comparisonsMean over 200 random inputs at each size, both counts on one pair of axes. Insertion sort performs more comparisons than Merge sort at every size measured, including n = 4: the dashed pair never cross. The solid pair — total reads and writes — do cross, between n = 5 and n = 6. The familiar advice to fall back to insertion sort on small subarrays is right, and the reason is memory traffic rather than comparisons, which is a distinction the usual telling of it loses.481632641281010010³10⁴noperations (mean of 200 runs)Insertion trafficMerge trafficInsertion cmpMerge cmptraffic crossessolid: reads + writes · dashed: comparisonstraffic crosses between n = 5 and 6; comparisons never do
Fig. 6 And the original input over two hundred trials rather than sixty. The crossing does not move, which is what makes it a threshold rather than an artefact of how many runs went into the mean.

Where the threshold should actually come from

Since this site cannot settle the exact cutoff, it is worth saying what would.

The threshold depends on the ratio between the cost of a comparison and the cost of moving an element, and that ratio depends on what is being sorted. Sorting 32-bit integers, a comparison and a move cost about the same and the traffic count is the right guide. Sorting 200-byte records by value, a move is fifty times a comparison and the threshold should be much lower — possibly zero, because merge sort’s comparison advantage is worth more than its traffic disadvantage almost immediately. Sorting pointers to objects, comparisons chase pointers and become expensive again, pushing the threshold back down.

So there is no single right cutoff, and the 16 or 32 that implementations use is a number tuned for the common case of small scalar elements. A library sorting arbitrary user types with a user comparator is choosing a threshold on behalf of workloads it has never seen, which is why some implementations expose it and most simply pick a defensible middle.

The general form of this is the same as choosing a growth factor for a dynamic array: a constant that no complexity class constrains, chosen by measurement, with different implementations landing in different places for reasons that are visible once the trade is drawn.

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. 7 Why the element type changes the answer. Comparisons on one axis and modelled memory behaviour on the other. Where an algorithm sits on this plot is fixed; how much each axis costs is a property of the data being sorted, and it is the thing a library cannot know in advance.

The overhead is a shrinking fraction

The cutoff’s cost is quoted above as a 26% rise in the comparison constant, and the arithmetic behind it says something the single number does not.

A cutoff of cc removes the bottom log2c\log_2 c levels of the merge recursion and replaces them with insertion sort on n/cn/c subarrays of cc elements each. The merging removed is about nlog2cn \log_2 c comparisons; the insertion sorting added is about nc/4n c / 4. So the extra comparisons are proportional to ncn c, and the total is proportional to nlog2nn \log_2 n — which makes the overhead

extratotal    clog2n\frac{\text{extra}}{\text{total}} \;\approx\; \frac{c}{\log_2 n}

a falling fraction rather than a constant. At n=64n = 64 the denominator is 6 and at n=4,096n = 4{,}096 it is 12, so the overhead over the measured range should roughly halve.

The fitted 1.059 is one constant over that range, which averages the drift rather than showing it. Both statements are true of the same data: the hybrid pays 26% more comparisons than plain merge sort over the sizes measured, and the share it pays falls as the input grows.

That reconciliation matters for the practical question. A fixed cutoff gets cheaper in relative terms the larger the sort, so a library choosing 16 is choosing a number whose cost is worst on the small inputs where the traffic saving is largest and best on the large inputs where it barely registers. The two effects point the same way, and the coincidence is what makes a single constant defensible across the whole range a library has to serve.

It also gives the sizing rule for anybody who wants to choose cc deliberately: the overhead is c/log2nc/\log_2 n, so holding it under a stated fraction means cβlog2nc \le \beta \log_2 n — which for a tenth and a million elements is two, and for a quarter and a thousand is two and a half. Every real cutoff is far above that, which says the comparison overhead is not what anybody is optimising, and returns the argument to the traffic count and the three unmeasured effects.

Why this threshold is rightly absolute

The cutoff is an integer with no nn in it, and that is worth defending, because this collection has elsewhere complained about exactly that shape.

A threshold expressed as a bare number is calibrated against a machine and ages as the machine changes. That complaint is right about a gallop threshold or a minimum run length, where the number is standing in for a ratio between two costs.

Here it is not. The cutoff’s job is to catch subarrays below the crossover, and the crossover is a property of the two algorithms measured against each other — the band between twelve and thirty-two elements identified above. That band does not move with nn: a subarray of sixteen elements is the same problem whether it came from sorting a hundred elements or a billion, and the algorithm cheaper on it is the same either way.

So the parameter is correctly absolute, and it is correctly a small integer. What it is calibrated against is the element type and the relative cost of a comparison against a move, which is the point the previous section makes — and that is a calibration a library performs once for scalars and cannot perform at all for a user-supplied comparator.

The reversed-input surprise

There is a coda, and it is the strangest measurement on the site.

Merge sort with a cutoff, on reversed input, does not fit nlognn \log n over the range this site normally measures. Fitted from n = 64 to n = 4,096 it fits linear better — a spread of 1.69 against 1.92 — and only when the range is extended to n = 65,536 does the ranking reverse.

The cause is the cutoff. On reversed input the insertion-sort phase is at its worst, costing about c2/2c^2/2 per subarray for cutoff cc, and that contributes a term proportional to nn which dominates below a few thousand elements. The merging contributes the nlognn \log n term and it is small on reversed input, because merging two runs where every element of one exceeds every element of the other terminates early.

The site’s fitting machinery refused to grant either class, which is correct, and the case became an essay about what measurement cannot establish. It is worth mentioning here because it is the same phenomenon as the crossover: a hybrid algorithm’s cost is a sum of two terms with a crossover between them, and every summary that reports one number is reporting whichever term happened to dominate over the range that was measured.

One of the three effects is now measurable

The first of the three effects listed above — that recursion costs something no array count sees — stopped being a hand-wave when the space instrument was built.

Merge sort makes 8,192 function calls to sort 4,096 elements. The hybrid, with its cutoff at 16, makes 512. That is a factor of sixteen in call-setup work, exactly, and it is a mechanism rather than an assertion.

It is still not a duration, and this site still does not have one. What it is is the right order of magnitude to account for a crossover that the traffic count only just explains, measured in a quantity that had no counter when this essay was written.

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 20 that link here.

The objects this essay names

Each one links to every other essay that touches it.

CacheComparison countConstant factorCrossoverCutoffHybrid sortMemory trafficRankingThreshold