Where insertion sort actually wins
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.
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.
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 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 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 in the worst case, so merge sort is at the floor here. It cannot be beaten.
Insertion sort at 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.
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.
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.
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 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 per subarray for cutoff , and that contributes a term proportional to which dominates below a few thousand elements. The merging contributes the 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.