What the libraries do

The buffer a schedule cannot bend

A stable merge sort that reads its input's runs first needed 36% to 45% of the input as buffer, because a balanced schedule's last merge joins two halves of the file. The repair proposed was to set the buffer first and choose a schedule whose merges fit it. No schedule can: merging the smallest pieces first still ends with two large pieces on almost every input, and every merge that does not fit is split by rotation. The extra moves are all rotations, six to eight and a half a key at a sixteenth of the input on every input, so the two costs simply add. Reading the runs still beats the library at every buffer on every input made of runs, and the schedule matters only where one run is most of the file.

A request that reads the input first gave a stable merge sort one pass over its input before it asked for any memory. The pass found the natural runs, and the moves fell with them: 2.80 a key on four long runs against 17.2 for libstdc++'s std::stable_sort, and none on sorted input. The buffer did not fall. Asked for as the largest merge’s shorter run, it came to between 36% and 45% of the input on everything from two runs to 4,096, against the library’s half. A balanced schedule merges runs in pairs, then pairs of pairs, and its last merge joins two halves of the file whatever the runs looked like.

That essay closed by turning the question round. The schedule is a choice, and the sort that has read the runs knows enough to make another. Set the buffer first — a quarter, an eighth, a sixteenth of the input — and choose a schedule whose every merge fits, using rotation for any merge that cannot. The prediction was that on a few long runs a sixteenth would cost little, because the long runs could be merged last against short neighbours, and that on random keys the sort would cost what the library costs at a sixteenth. The question was whether reading the input lets a sort set its memory by the input’s disorder rather than its length.

Two schedules and a split

The sort is the earlier page’s, with the minimum run of eight that page found better for moves than thirty-two — one of the constants the threshold somebody chose found reasonable and underived in every library that carries one. One pass finds the maximal non-decreasing runs, and runs shorter than eight are extended by binary insertion. The merges are then scheduled in one of two ways. Balanced rounds is the earlier page’s schedule: adjacent runs in pairs, round by round. Smallest-first repeatedly merges the adjacent pair whose shorter run is shortest, so every merge that fits the buffer is taken before any merge that does not. It is the greedy form of what the proposal described, and it is close to what Timsort’s merge rules aim at, keeping pending runs of balanced size so that merges stay cheap.

A merge whose shorter run fits the buffer copies that run into the buffer and merges from it, as the library’s buffered merge does. One that does not fit is split. The library’s own routine for that case halves the longer run, finds the matching cut in the shorter one by binary search, rotates the middle so that both halves’ pieces are in place, and recurses on the two halves until each piece fits. The rotation moves keys without merging anything. A guarantee that depends on an allocation found that path carrying the library’s sort when its buffer request fails, and it is the same code here, chosen deliberately.

The inputs are the earlier page’s: 65,536 keys made of four, sixteen, 256 or 4,096 ascending runs of random lengths, a sorted file with a tenth of new keys appended in random order, and random keys. Every sort is checked sorted and stable, and moves and comparisons are counted as before, every element assignment a move.

What a buffer set in advance costs

A buffer set in advance, on 65,536 keys: on four long runs, reading the runs first costs 2.80 moves a key with room for every merge, 3.53 at a quarter, 8.70 at a sixteenth and 20.88 at a 256th — below the library at every buffer, which moves 17.21 a key at half; on random keys the two are within 14% of each other at every bufferElement moves a key against the buffer the sort is given, as a share of the input, from a half down to a 256th. Four runs, runs read first: 1/2 2.80, 1/4 3.53, 1/8 5.70, 1/16 8.70, 1/64 14.69, 1/256 20.88. Four runs, the library: 1/2 17.21, 1/4 17.25, 1/8 20.45, 1/16 22.94, 1/64 29.99, 1/256 35.83. Random keys, runs read first: 1/2 20.92, 1/4 21.62, 1/8 24.06, 1/16 28.37, 1/64 41.07, 1/256 60.60. Random keys, the library: 1/2 18.71, 1/4 18.97, 1/8 22.90, 1/16 27.20, 1/64 40.76, 1/256 60.50. Both axes are logarithmic.1/21/41/81/161/641/25625102050buffer, share of the inputmoves a keyfour runs, runs read firstfour runs, the libraryrandom keys, runs read firstrandom keys, the library65,536 keys, schedule smallest-firstdashed: random keys
Fig. 1 Moves a key against the buffer, as a share of the input. Four runs, runs read first: 2.80 at half, 3.53 at a quarter, 5.70 at an eighth, 8.70 at a sixteenth, 14.7 at a sixty-fourth, 20.9 at a 256th. The library on four runs: 17.2 to 35.8. Random keys, runs read first: 20.9 to 60.6; the library 18.7 to 60.5.

On four long runs, a buffer of a sixteenth costs 8.70 moves a key against 2.80 with room for every merge — three times as many. A quarter costs 3.53, an eighth 5.70, a sixty-fourth 14.7 and a 256th 20.9. The prediction said a sixteenth would cost little, and it costs three times the moves. It is still half the library’s 17.2 at its full request of half the input, and under two fifths of the library’s 22.9 at the same sixteenth. The run-reading sort moves fewer keys than the library at every buffer measured, down to a 256th.

On random keys the run-reading sort costs what the library costs, within 14% at every buffer. At a sixteenth it moves 28.4 keys a key against the library’s 27.2, and at a 256th 60.6 against 60.5. That part of the prediction holds. Random keys have runs two keys long, the pass finds nothing worth knowing, and both sorts are then merge sorts over the same buffer with the same rotations.

Every extra move is a rotation

Every extra move a small buffer causes is a rotation: at a sixteenth of the input the merging and buffering moves are the same as with room for every merge, and rotation adds 5.90 moves a key on four runs, 8.50 on 256, 7.45 on random keys — about the same whatever the input — and 0.55 on a sorted file with a tenth appendedMoves a key with a buffer of a sixteenth of the input, schedule smallest-first, split by what they do: extending short runs by insertion, copying a run into the buffer, merging, and rotating to split a merge that does not fit. Four runs: extending short runs 0.00, into the buffer 0.80, merging 2.00, rotating, to fit 5.90; in all 8.70 (2.80 with room for every merge). Sixteen runs: extending short runs 0.00, into the buffer 1.59, merging 3.88, rotating, to fit 7.43; in all 12.91 (5.48 with room for every merge). 256 runs: extending short runs 0.12, into the buffer 3.18, merging 7.69, rotating, to fit 8.50; in all 19.48 (10.99 with room for every merge). 4,096 runs: extending short runs 0.80, into the buffer 4.86, merging 11.35, rotating, to fit 7.29; in all 24.31 (17.02 with room for every merge). A tenth appended: extending short runs 0.23, into the buffer 0.52, merging 1.96, rotating, to fit 0.55; in all 3.26 (2.71 with room for every merge). Random keys: extending short runs 2.29, into the buffer 5.61, merging 13.02, rotating, to fit 7.45; in all 28.37 (20.92 with room for every merge).extending short runsinto the buffermergingrotating, to fitfour runs8.70sixteen runs12.91256 runs19.484,096 runs24.31a tenth appended3.26random keys28.37a sixteenth of the inputmoves a key, smallest-first schedule
Fig. 2 Moves a key at a sixteenth of the input, split by what they do. Four runs: merging 2.00, into the buffer 0.80, rotating 5.90. Sixteen runs: 3.88, 1.59, 7.43. 256 runs: 7.69, 3.18, 8.50. Random keys: 13.0, 5.61, 7.45, with 2.29 extending short runs. A tenth appended: 1.96, 0.52, 0.55.

At a sixteenth of the input, the merging and buffering moves are exactly what they were with room for every merge; everything added is rotation. On four runs the rotations cost 5.90 moves a key, on sixteen 7.43, on 256 8.50, on 4,096 7.29 and on random keys 7.45. The merging itself costs 2.00 on four runs and 13.0 on random keys, the log of the number of runs as before. At a sixteenth the rotation cost barely follows the runs. It sits between six and eight and a half moves a key on every input but one, and it is set by the buffer.

That is why the prediction failed. It pictured the long runs merged last against short neighbours, so that only the final merges would outgrow the buffer. On four runs of random lengths there are no short neighbours. The four runs average 16,384 keys, every merge in any schedule joins two pieces of thousands of keys, and all three merges are split. A split merge of mm keys with a buffer of bb recurses until its pieces fit, about log⁡2(m/b)\log_2(m/b) levels deep, and each level rotates up to the whole merge’s width. With a buffer of a sixteenth, that is a few levels of rotation over most of the file, whatever order the merges came in.

Below a sixteenth the inputs separate. At a sixty-fourth, rotation costs 11.9 moves a key on four runs and 20.2 on random keys; at a 256th, 18.1 and 39.7. Each halving of the buffer adds a level of splitting to every merge too large for it, and random keys have more rounds of merges large enough to be split: about thirteen rounds over the whole file against two on four runs. Four runs add about three moves a key in rotation for each halving below a sixty-fourth, and random keys about ten. Still, the buffer decides whether rotation is paid at all, and the runs decide how many times.

So the two costs add. Reading the runs sets the merging cost, the buffer sets the rotation cost, and neither changes the other. The one input where the rotations nearly vanish, the sorted file with a tenth appended, costs 0.55 moves a key in rotation. It is different because of what the schedule can do there, not the buffer.

What the split costs in comparisons

A split merge finds its cut by binary search, and the searches are the only comparisons a rotation adds. They are almost free. On four runs the sort makes 3.00 comparisons a key with room for every merge and 3.03 at a 256th of the input; on random keys, 15.96 and 16.07. The library makes 10.2 comparisons a key on four runs and 15.8 on random keys at every buffer. So on an input with long runs, reading the runs first saves two thirds of the comparisons and most of the moves, and a small buffer takes back only moves.

That matters for which cost a caller should watch. A run is a property of the input found the number of natural runs predicting a presorted input’s comparison count better than any recipe for making presorted data. The same count predicts the comparisons here at every buffer. The moves it predicts only when the buffer is large enough for the merges, and below that the buffer predicts them too. The order equal keys keep is the reason none of this can be bought more cheaply by giving up stability: a rotation is how a merge with no room to copy keeps equal keys in the order they arrived, and that order is what the caller of a stable sort paid for.

No order of merges avoids two large pieces

The buffer each schedule would need to never rotate: on four runs 45% of the input under either schedule, on sixteen 43%, on random keys 41% smallest-first against 45% balanced — no order of merges avoids a last merge between two large pieces; only on a sorted file with a tenth appended does it fall, to 10% smallest-first and 3% balancedFor each input, the shorter run of the largest merge each schedule performs: the buffer it would need so that no merge is split. Four runs: smallest-first 45.1%, balanced 45.1%. Sixteen runs: smallest-first 43.4%, balanced 43.4%. 256 runs: smallest-first 34.6%, balanced 41.9%. 4,096 runs: smallest-first 43.0%, balanced 36.0%. A tenth appended: smallest-first 10.0%, balanced 3.4%. Random keys: smallest-first 40.6%, balanced 45.3%.0%13%25%38%50%buffer needed, share of the inputfour runssixteen runs256 runs4,096 runsa tenth appendedrandom keyssmallest-firstbalanced65,536 keysthe largest merge's shorter run
Fig. 3 The shorter run of the largest merge each schedule makes: the buffer it would need to never rotate. Four runs: 45% under either schedule. Sixteen: 43%. 256 runs: 35% smallest-first, 42% balanced. 4,096: 43% and 36%. Random keys: 41% and 45%. A tenth appended: 10% smallest-first, 3.4% balanced.

Merging smallest-first needs 45% of the input on four runs, 43% on sixteen and 41% on random keys to avoid every rotation — about what the balanced schedule needs, and on 4,096 runs more, 43% against 36%. The last merge of any schedule joins two pieces that together are the whole file. Its shorter piece is as small as the schedule can make it only if one side can be kept small to the end, and that requires a run that is most of the file. With runs of comparable length, the two final pieces are comparable too, whatever order built them. Smallest-first can shift which merges are last, and it does lower the need on 256 runs, 35% against 42%. It cannot make the last merge lopsided when the input is not.

The sort’s memory is therefore set by the shape of its largest runs, and a schedule can only choose among a few orders of the same pieces. Disorder in the sense the earlier page measured, the number of runs, does not enter it. The proposal’s hope, that reading the input would let the memory follow the disorder rather than the length, is met only where the input’s disorder is concentrated in a small part of it.

Where the schedule matters

Where the schedule matters: a sorted file of 65,536 keys with a tenth appended in random order — merging smallest-first costs 2.71 moves a key down to a buffer of an eighth and 3.26 at a sixteenth; in balanced rounds it costs 10.42 at every buffer to a sixteenth, because the long sorted run is merged again in every round; the library 15.95 to 19.42Moves a key on a sorted file with a tenth of new keys appended in random order, against the buffer set in advance, for the two schedules and the library. Smallest-first: 1/2 2.71, 1/4 2.71, 1/8 2.71, 1/16 3.26, 1/64 6.56, 1/256 10.43. Balanced rounds: 1/2 10.42, 1/4 10.42, 1/8 10.42, 1/16 10.42, 1/64 14.89, 1/256 26.19. The library: 1/2 16.92, 1/4 15.95, 1/8 16.89, 1/16 16.38, 1/64 17.57, 1/256 19.42. Merges split for want of buffer, smallest-first: 0, 0, 0, 1, 4, 16; balanced: 0, 0, 0, 0, 4, 22. Both axes are logarithmic.1/21/41/81/161/641/256251020buffer, share of the inputmoves a keysmallest-firstbalanced roundsthe librarya sorted file, a tenth appendedthe run found first is 90% of the file
Fig. 4 A sorted file of 65,536 keys with a tenth appended in random order. Smallest-first: 2.71 moves a key down to a buffer of an eighth, 3.26 at a sixteenth, 6.56 at a sixty-fourth, 10.4 at a 256th. Balanced rounds: 10.4 down to a sixteenth, 14.9 and 26.2 below. The library: 16.0 to 19.4.

On a sorted file with a tenth appended, merging smallest-first costs 2.71 moves a key at every buffer down to an eighth, and balanced rounds cost 10.4. Here the input has one run that is 90% of the file and a tail of short runs. Smallest-first merges the tail’s runs among themselves until they form one run of a tenth of the file, then merges that once into the long run. Its largest merge’s shorter side is the tail, 10% of the input, and a buffer of an eighth holds it. Balanced rounds pair the long run with the first short run next to it in the first round, then with the result of the next pair, and so on. The long run is merged again in every round, and each round moves most of the file. Its largest shorter side is smaller, 3.4%, but it pays for that in moves four times over.

At a sixteenth the smallest-first sort splits only its last merge and costs 3.26 moves a key; at a sixty-fourth, 6.56. The library, which reads nothing, costs 16 to 19 at every buffer. This is the case the earlier page found the request falling on, and it is where choosing the schedule, not only sizing the buffer, pays: by a factor of four at the same buffer. When galloping pays found galloping worth having on inputs of exactly this shape, and a merge that gallops over the long run’s untouched stretches would cut the 2.71 further. Galloping was not modelled here.

The margin over a sort that does not look

Reading the runs first, as a share of the library's moves at the same buffer: on four runs 0.16 with room for every merge and 0.58 at a 256th — the advantage narrows as the buffer shrinks, because both pay the same rotations, and it never closes; on random keys the share stays between 1.00 and 1.14Moves a key of the sort that reads its runs first (smallest-first schedule) divided by the library's moves with the same buffer, against the buffer as a share of the input. Four runs: 1/2 0.16, 1/4 0.20, 1/8 0.28, 1/16 0.38, 1/64 0.49, 1/256 0.58. Sixteen runs: 1/2 0.32, 1/4 0.35, 1/8 0.41, 1/16 0.52, 1/64 0.65, 1/256 0.72. 256 runs: 1/2 0.64, 1/4 0.69, 1/8 0.71, 1/16 0.77, 1/64 0.83, 1/256 0.88. 4,096 runs: 1/2 0.97, 1/4 0.99, 1/8 0.92, 1/16 0.94, 1/64 0.95, 1/256 0.96. A tenth appended: 1/2 0.16, 1/4 0.17, 1/8 0.16, 1/16 0.20, 1/64 0.37, 1/256 0.54. Random keys: 1/2 1.12, 1/4 1.14, 1/8 1.05, 1/16 1.04, 1/64 1.01, 1/256 1.00. Both axes are logarithmic.1/21/41/81/161/641/2560.10.20.51buffer, share of the inputmoves, as a share of the library'sfour runssixteen runs256 runs4,096 runsa tenth appendedrandom keys65,536 keysbelow the line: fewer moves than the library
Fig. 5 Moves as a share of the library’s at the same buffer. Four runs: 0.16 at half, 0.58 at a 256th. Sixteen runs: 0.32 and 0.72. 256 runs: 0.64 and 0.88. 4,096 runs: 0.96 and 0.96. A tenth appended: 0.16 and 0.54. Random keys: 1.00 to 1.14.

Reading the runs first moves 16% of the library’s moves on four runs with room for every merge, and 58% at a 256th of the input. The margin narrows as the buffer shrinks because both sorts pay about the same rotations, and a shared cost that grows dilutes a fixed saving. It never closes on any input made of runs. On 4,096 runs the margin is 4% throughout, since runs sixteen keys long leave the pass little to find. On random keys the run-reading sort is up to 14% worse, at a quarter, and level with the library at the smallest buffers.

That is the practical form of the answer. A library that reads its input first and is handed a small buffer — because the allocation failed, or because the caller set a budget — loses none of the advantage the pass gave it, beyond what the rotations dilute. The request no price of memory would make found the library’s own request of half the input on no memory price’s best line. This page’s numbers say a sort that has read the runs can live on a sixteenth of the input and still move half what the library moves at a half, on inputs of a few long runs.

What was wrong in the picture

The proposal’s picture of a schedule came from inputs it imagined: long runs with short neighbours, where the order of merges decides which pieces meet the buffer. Random run lengths do not look like that. Four cuts at random positions make four runs of comparable size, and the same is true of sixteen or 256. The only common input with the imagined shape is a long run with a short tail, and there the picture was right.

The sort the library ships found every major runtime but C++ sorting objects with a sort that reads runs first. The results here add one point to that. Those sorts also merge in an order chosen from the runs, and the choice mattered here only on appended data. Everywhere else the rotations set the price of a small buffer, and rotations are the same code in every library.

Where the measurement stops

Moves, not time. Every number is an element move. A rotation moves keys in long sequential sweeps and a merge interleaves two sources, so a timed comparison could weigh the two differently. Counting instead of timing is why the counts are what is reported.

Two schedules. Smallest-first is a greedy rule. A schedule chosen by searching over merge trees for the fewest moves at a given buffer might do better on 256 runs, where the greedy rule already lowers the need. It was not searched for.

One draw of run lengths an input. Each input with kk runs is one draw of random cut points. On four runs, a draw with one very short run would give smallest-first something to merge early; the draw here has runs of 16%, 29%, 35% and 19% of the file, none short enough to merge early.

One input size. Every measurement is at 65,536 keys. A split merge’s depth follows the ratio of the merge’s width to the buffer, so a larger file with the same share of buffer should split each merge to the same depth, and one with the same absolute buffer should split deeper. Neither was run.

No galloping and no descending runs. As before, the pass finds non-decreasing runs only, since reversing a run could reorder equal keys, and no merge gallops.

Still open: a rotation that knows the runs

Every extra move a small buffer causes is a rotation, and the library’s rotation is a generic one. It halves the longer run, binary-searches the shorter, rotates the middle and recurses, as if the two runs were arbitrary. A sort that has read its runs knows more than that about the two sides of a split merge. It knows where the natural runs inside each side began, and whether a long stretch of one side falls entirely before or after the other side’s keys.

The measurement that follows gives the split a first step: before halving, find by galloping how much of each side is already in place against the other, and exclude it from the rotation. The prediction is that on inputs of a few long runs with random values, which interleave everywhere, this saves almost nothing, since little of any side is already in place. On inputs whose runs overlap only at their ends it should remove most of the rotation cost. That includes a sorted file with a batch of late keys, or runs that were themselves sorted batches of increasing keys. The number that decides it is the share of each split merge that galloping can set aside. It is also the share of the input that a stable sort with a small buffer never needed to move at all.

Named alongside this one

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

The objects this essay names

Each one links to every other essay that touches it.

Data movementDesign parameterHonest limitLibrary sortMerge sortNatural runRotationSpace time tradeStability