What a bound is

The lines a sort writes back

A replay that charges a transfer for every line brought into fast memory and none for a line sent back flatters every sort that writes in place. Charged for its write-backs, quicksort's transfers rise by 1.9 at every memory short of n, as predicted, and heapsort's by 1.8, not the predicted three, because no line can go back more often than it came in. The cost that changes the account is the last one: at n itself every in-place sort pays two reads of its array, so none of them can come within a factor of two of the floor, while the read-only family reaches 1.07.

The memory a product does not charge replayed every sort’s array accesses through an LRU memory of MM cells in lines of eight and read three numbers off the replay: the transfers a sort pays, the cells it writes, and the fast memory it needs. A read-only family that rescans its input for every buffer of values it emits needs almost no fast memory and stays near the floor of n2/Bn^2/B on transfers times writable cells. Quicksort writes into its array and needs an eighth of nn in fast memory to stay within a factor of eight of that floor. And transfers times fast memory, the product the published bound never takes, separated the two kinds of sort completely: the family never fell below 1.29 times n2/Bn^2/B, and quicksort reached 0.05.

That replay charged a transfer when a line came into fast memory and nothing when one went out. Its closing section named whom that flatters. The family writes only its output, once, in order. Every sort that writes in place modifies the lines it reads, and a modified line must go back to slow memory before its slot can be reused. It predicted that charging those write-backs would roughly double quicksort’s transfers at small memories and nearly triple heapsort’s, whose sift-downs modify lines and leave them quickly, and that the family’s transfers would not move. On transfers times fast memory it predicted quicksort’s least value would rise from 0.05 to about 0.1, still far below the family’s 1.29.

Two of those predictions hold, one is impossible, and one fails in a way that exposes something the family was getting for free.

Every access marked as a read or a write

The sorts are the earlier page’s, on the same random keys: heapsort, Shellsort, and quicksort with a random pivot, with a median-of-three pivot and with the first element as pivot. Each runs on a counted array that now records, beside every address it touches, whether the touch wrote. A comparison reads two cells, a swap reads two and writes two, and a store writes one. The family is traced the same way. Its scans read the input and the largest entry of its buffer, its insertions and the shifts that make room for them write the buffer, its output is written once in order, and the cell holding the last value emitted is written each time a value goes out.

Each trace is replayed through LRU with the two rules a real write-back cache uses. A miss fetches the line whether the access that missed reads or writes, which is write-allocate. A write marks the line dirty, and evicting a dirty line costs a second transfer. When the run ends, every line still dirty is written back too, because an array that is sorted in fast memory is not yet sorted where anyone else can read it. The replay’s misses are checked against the earlier simulator’s on the same traces and agree exactly, so the read side of every number here is the earlier page’s. One access, eight kilobytes introduced this way of charging an access by the line it lands on; this is the same model with the line’s return trip added.

One fact settles part of the prediction before any trace runs. A line comes in once each time it misses and goes out at most once each time it is evicted, so the write-backs over a whole run can never exceed the misses plus the lines left dirty at the end. Charging write-backs can at most double a sort’s transfers. Heapsort cannot triple.

Nine lines in ten come back dirty

Of the lines quicksort brings into fast memory, 89% to 96% go back dirty at every memory short of n, and of heapsort's 79% to 86%; the read-only family writes back almost nothing once its buffer fits, and 45% of its lines when a buffer of 64 must share 32 cellsWrite-backs — dirty lines evicted, plus the dirty lines left at the end — as a share of misses, against fast memory M in cells, n = 8,192, lines of 8, LRU with write-allocate. One means every line brought in is written back. Heapsort: 16 0.788, 32 0.828, 64 0.829, 128 0.808, 256 0.813, 512 0.819, 1024 0.829, 2048 0.841, 4096 0.861, 8192 1.000. Shellsort: 16 0.534, 32 0.821, 64 0.863, 128 0.882, 256 0.895, 512 0.910, 1024 0.934, 2048 0.951, 4096 0.983, 8192 1.000. Quicksort, random pivot: 16 0.925, 32 0.912, 64 0.911, 128 0.908, 256 0.902, 512 0.892, 1024 0.894, 2048 0.898, 4096 0.956, 8192 1.000. Family, 16 cells: 16 0.014, 32 0.053, 64 0.027, 128 0.018, 256 0.012, 512 0.008, 1024 0.005, 2048 0.003, 4096 0.003, 8192 0.003. Family, 64 cells: 16 0.099, 32 0.449, 64 0.400, 128 0.177, 256 0.074, 512 0.026, 1024 0.012, 2048 0.009, 4096 0.009, 8192 0.009. Family, 256 cells: 16 0.515, 32 0.832, 64 0.842, 128 0.845, 256 0.701, 512 0.101, 1024 0.043, 2048 0.035, 4096 0.033, 8192 0.032. Both axes are logarithmic.1664256102440960.010.11fast memory M, cellswrite-backs per line brought inevery line comes backheapsortShellsortquicksort, random pivotfamily, 16 cellsfamily, 64 cellsfamily, 256 cellsn = 8,192, lines of 8, LRU, write-backone: every line
Fig. 1 Write-backs as a share of the lines brought in, against fast memory, n = 8,192, lines of eight. Quicksort with a random pivot: 0.89 to 0.93 at every memory from 16 cells to 2,048, 0.96 at 4,096. Heapsort: 0.79 to 0.86. Shellsort: 0.53 at 16 cells, then 0.82 to 0.98. The read-only family with a buffer of 16: 0.003 to 0.05. With a buffer of 64: 0.45 at 32 cells, 0.009 from 2,048. With 256: 0.85 at 128 cells, 0.03 to 0.04 from 1,024.

Of the lines quicksort brings into fast memory, 89% to 93% go back dirty at every memory from 16 cells to a quarter of nn, and of heapsort’s, 79% to 86%. A partition reads a line, swaps some of its keys with keys from the other end of the range, and moves on. On random keys almost every line it crosses holds a key on the wrong side, so almost every line it reads it also writes. The share does not depend on how much fast memory there is, because a partition’s sweep reads and dirties each line once per level of the recursion whatever MM is; memory decides how many of those levels fit, which changes how many lines come in, not what happens to each one.

Heapsort’s share is lower and steadier. A sift-down compares a key with its two children and swaps it with the larger, level by level, until it is larger than both. Every line on that path is written except the last, where the comparison found nothing to move, and the stop at the bottom of each sift-down leaves a line or two read and clean. That reason is the likeliest one; the replay counts the lines and not which comparison touched them last. Shellsort at 16 cells writes back only 53% of what it reads, the likeliest reason being that with two lines of fast memory its long-gap comparisons fetch lines it then does not modify; above that its share climbs towards one as its gaps shrink and its passes become near-sequential insertion sorts that move a key on almost every line.

The family is the other extreme, and each of its curves is two curves joined at its buffer. Once the buffer fits, what comes back dirty is mostly its output, about one line for every line of output, against the 33 to 513 reads of the array its scans make, so the share is a few lines in a hundred or less. Where a buffer of 64 cells must share 32 cells of fast memory, 45% of the lines it brings in go back dirty; where a buffer of 256 shares 128, 85%. The family writes in two places, and only its output is cheap to write.

Never more than double, and two at the end

Charging the write-backs multiplies quicksort's transfers by 1.92 at 16 cells and heapsort's by 1.79, never more than 1.98 for any sort at any memory short of n — and at n itself every in-place sort pays two reads of its array, one to bring it in and one to put it backBlock transfers in reads of the whole array (n/B), reads only and with write-backs, against fast memory, n = 8,192, lines of 8, LRU. Heapsort: 16 cells 132.8 → 237.4, 32 cells 104.3 → 190.8, 64 cells 100.4 → 183.7, 128 cells 67.3 → 121.6, 256 cells 55.3 → 100.3, 512 cells 42.6 → 77.5, 1024 cells 30.0 → 54.9, 2048 cells 18.4 → 33.9, 4096 cells 8.7 → 16.2, 8192 cells 1.0 → 2.0. Shellsort: 16 cells 211.2 → 323.9, 32 cells 89.0 → 162.1, 64 cells 44.6 → 83.2, 128 cells 31.1 → 58.5, 256 cells 28.8 → 54.5, 512 cells 23.7 → 45.3, 1024 cells 18.8 → 36.4, 2048 cells 16.9 → 32.9, 4096 cells 11.9 → 23.6, 8192 cells 1.0 → 2.0. Quicksort, random pivot: 16 cells 25.2 → 48.5, 32 cells 16.7 → 32.0, 64 cells 14.1 → 27.0, 128 cells 12.1 → 23.0, 256 cells 10.2 → 19.4, 512 cells 8.5 → 16.1, 1024 cells 6.6 → 12.6, 2048 cells 5.1 → 9.6, 4096 cells 2.7 → 5.2, 8192 cells 1.0 → 2.0. Both axes are logarithmic. The left-hand labels are the curves at 16 cells.166425610244096110100fast memory M, cellstransfers, in reads of the whole arrayheapsort, chargedheapsort, reads onlyShellsort, chargedShellsort, reads onlyquicksort, random pivot,chargedquicksort, random pivot,reads onlyn = 8,192, lines of 8pale: the reads alone
Fig. 2 Transfers in reads of the whole array, reads only (pale) and with write-backs charged, n = 8,192, lines of eight. Quicksort, random pivot: 25.2 → 48.5 at 16 cells, 8.5 → 16.1 at 512, 2.7 → 5.2 at 4,096, 1 → 2 at 8,192. Heapsort: 132.8 → 237.4, 42.6 → 77.5, 8.7 → 16.2, 1 → 2. Shellsort: 211.2 → 323.9, 23.7 → 45.3, 11.9 → 23.6, 1 → 2.

Charged for its write-backs, quicksort pays 1.92 times its read transfers at 16 cells and between 1.89 and 1.96 at every other memory short of nn; heapsort pays 1.79 times at 16 cells and never more than 1.86. The first is the prediction’s “roughly double”. The second is not “nearly triple”, and the plate shows why the prediction could not have held: a sort’s transfers with write-backs charged are its misses times one plus its share, and the share is a fraction. The worst ratio at any memory short of nn, for any sort here, is 1.98, Shellsort’s at 4,096 cells, where nearly every line it reads is modified on the same pass.

What the prediction did not mention is the right-hand end. With the whole array in fast memory, every in-place sort pays one read of its array to bring it in, and on the old account that was the whole bill. Charged for its write-backs, it pays a second read’s worth to send the sorted array back out, because on random keys every line has had some key move. That makes the cheapest an in-place sort can possibly be, in any memory, two reads of its array. It is the floor of permuting is sorting here, seen from the other side: a sort that rearranges nn keys in place has written every line of them, and writing a line to slow memory costs what reading it did.

A factor of two out of reach

With write-backs charged, no in-place sort's transfers times writable cells comes within a factor of two of n²/B at any memory: heapsort and quicksort reach 2.00 and 2.01 only with all of n in fast memory, while the read-only family reaches 1.07 with a buffer of 64 and 1.19 with 16Block transfers, write-backs included, times writable cells, over n²/B, against fast memory, n = 8,192, lines of 8, LRU. Heapsort: 16 237.4, 32 190.8, 64 183.8, 128 121.6, 256 100.3, 512 77.5, 1024 54.9, 2048 33.9, 4096 16.2, 8192 2.00. Shellsort: 16 324.0, 32 162.1, 64 83.2, 128 58.6, 256 54.5, 512 45.3, 1024 36.4, 2048 32.9, 4096 23.6, 8192 2.00. Quicksort, random pivot: 16 48.7, 32 32.1, 64 27.1, 128 23.1, 256 19.5, 512 16.1, 1024 12.6, 2048 9.65, 4096 5.22, 8192 2.01. Family, 16 cells: 16 15.2, 32 1.36, 64 1.25, 128 1.23, 256 1.22, 512 1.21, 1024 1.20, 2048 1.20, 4096 1.19, 8192 1.19. Family, 64 cells: 16 15.8, 32 3.91, 64 2.74, 128 1.51, 256 1.22, 512 1.10, 1024 1.07, 2048 1.07, 4096 1.07, 8192 1.07. Family, 256 cells: 16 38.6, 32 27.6, 64 26.3, 128 21.5, 256 6.34, 512 1.25, 1024 1.10, 2048 1.08, 4096 1.08, 8192 1.08. The rule at two is the least an in-place sort can pay: one read of its array and one write. Both axes are logarithmic.1664256102440961210100fast memory M, cellstransfers × writable cells, per n²/Btwo: one read, one write of nheapsortShellsortquicksort, random pivotfamily, 16 cellsfamily, 64 cellsfamily, 256 cellsn = 8,192, write-backs chargedthe floor at one
Fig. 3 Transfers with write-backs times writable cells, over n2/Bn^2/B, against fast memory, n = 8,192. Heapsort: 237 at 16 cells, 77.5 at 512, 16.2 at 4,096, 2.00 at 8,192. Quicksort, random pivot: 48.7, 16.1, 5.22, 2.01. Shellsort: 324, 45.3, 23.6, 2.00. The family with 16 cells: 1.36 at 32 cells, 1.19 from 4,096. With 64: 3.91 at 32, 1.07 from 1,024. With 256: 6.34 at 256, 1.25 at 512, 1.08. The rule at two is one read and one write of n cells.

With write-backs charged, no in-place sort’s transfers times writable cells comes within a factor of two of n2/Bn^2/B at any memory: heapsort and quicksort reach 2.00 and 2.01, and only with all of nn in fast memory. The read-only family reaches 1.07 with a buffer of 64 and 1.19 with 16. The earlier page’s verdict on this product was that every sort here can reach the floor given enough fast memory, and that the product therefore calls them equals. On the honest account, the in-place sorts’ floor is twice the family’s.

The arithmetic is short. An in-place sort’s writable cells are its nn array cells, and its transfers can never fall below the two reads of its array the last plate found, 2n/B2n/B. The product is at least 2n2/B2n^2/B. The family writes only SS cells, and its transfers are its n/Sn/S scans of n/Bn/B each plus its output, so its product is n2/Bn^2/B plus terms that shrink as SS grows and as n/Sn/S does. Its output costs it a read of the array to write as well, since an output line is fetched before it is written and written back after; but that read is one more scan among the hundreds the family already makes, a few per cent of its bill, where for an in-place sort with all of nn in fast memory it is half.

A floor under a product found the published bound easy to beat by four hundred times when the input may be written, and the earlier pages put the in-place sorts back near it by counting the array as writable memory and charging transfers instead of reads. With write-backs charged, the bound’s model and the block model now agree on a direction: writing into the input is a cost, and the read-only family is the cheapest way to sort that either model measures, by a factor of nearly two on this product even when fast memory is unlimited.

What every factor now costs in fast memory

At n = 8,192, charging the write-backs puts a factor of two out of every in-place sort's reach and raises quicksort's need for fast memory at every other factor — 8,192 cells within 4 instead of 4,096, 4,096 cells within 8 instead of 1,024, 1,024 cells within 16 instead of 64 — while the read-only family's need stays at its bufferThe least fast memory on a doubling grid at which block transfers times writable cells is within a factor of 2, 4, 8 and 16 of n²/B, n = 8,192, lines of 8: the pale bar with reads only, the solid bar with write-backs charged. Heapsort: within 2 8,192 → never, within 4 8,192 → 8,192, within 8 8,192 → 8,192, within 16 4,096 → 8,192. Quicksort, random pivot: within 2 8,192 → never, within 4 4,096 → 8,192, within 8 1,024 → 4,096, within 16 64 → 1,024. Quicksort, median of three: within 2 8,192 → never, within 4 8,192 → 8,192, within 8 1,024 → 8,192, within 16 64 → 1,024. Family, 16 cells: within 2 32 → 32, within 4 32 → 32, within 8 32 → 32, within 16 16 → 16. Family, 64 cells: within 2 64 → 128, within 4 32 → 32, within 8 32 → 32, within 16 16 → 16. Family, 256 cells: within 2 512 → 512, within 4 256 → 512, within 8 256 → 256, within 16 32 → 256. No in-place sort is within a factor of two once write-backs are charged.within 2within 4within 8within 16pale: reads only · solid: write-backs charged · ✕: never161281,0248,192heapsort✕quicksort, random pivot✕quicksort, median of three✕family, 16 cellsfamily, 64 cellsfamily, 256 cellsleast fast memory, cells (logarithmic)n = 8,192, lines of 8one pair of bars a factor
Fig. 4 At n = 8,192, the least fast memory on a doubling grid within a factor of 2, 4, 8 and 16 of n2/Bn^2/B, reads only (pale) and with write-backs charged (solid). Quicksort, random pivot: within 2, 8,192 then never; within 4, 4,096 then 8,192; within 8, 1,024 then 4,096; within 16, 64 then 1,024. Heapsort: within 2, 8,192 then never; all of n within 4 and 8; 4,096 then 8,192 within 16. The family with a buffer of 16: unchanged. With 64: 128 cells instead of 64 within 2, otherwise unchanged. With 256: 512 instead of 256 within 4, and 256 instead of 32 within 16.

At n = 8,192, quicksort with a random pivot now needs all of nn in fast memory to come within a factor of four of the floor, half of nn within eight, and an eighth within sixteen, where on reads alone it needed half, an eighth and a 128th. Each requirement has moved up by one to four doublings, and within a factor of two there is no memory at which any in-place sort qualifies. Heapsort, which needed half of nn within sixteen, now needs all of it within every factor it can reach.

The size of each move follows from the earlier page’s arithmetic. There, each halving of fast memory added about 1.8 reads of the array to quicksort’s bill, and the memory it needed fell exponentially in the factor allowed. Write-backs nearly double every one of those additions, to about 3.5 reads of the array a halving, and they double the starting point too, from one read of the array to two. A factor of sixteen allows about sixteen reads of the array. On reads alone quicksort starts at one and has room for about eight halvings at 1.8 each; charged, it starts at two and has room for about four at 3.5. The grid measures seven and three: 64 cells against 1,024.

The family’s requirements mostly stay where they were. A buffer of 16 needs exactly what it needed at every factor, a buffer of 64 needs 128 cells instead of 64 to come within a factor of two, and a buffer of 256 needs 512 instead of 256 within four. The one large move is the loosest factor on the largest buffer: within sixteen, a buffer of 256 needed only 32 cells of fast memory on reads alone, an eighth of itself, and charged it needs all 256. On reads alone it could afford to let most of its buffer live in slow memory; charged for the buffer’s write-backs, which the plate after next isolates, it cannot. The cliff where the data stops fitting is the same shape at the family’s scale: a working set that fits pays almost nothing, and one that nearly fits pays almost everything.

The product the floor does not take, repriced

Transfers times fast memory, write-backs charged: quicksort's least rises from 0.049 of n²/B to 0.095, and the read-only family's from 1.29 to 1.87, because the family's buffer is written too — the in-place sort stays 20 times under the familyBlock transfers, write-backs included, times fast memory M, over n²/B, against M, n = 8,192, lines of 8, LRU. Heapsort: 16 0.464, 32 0.745, 64 1.44, 128 1.90, 256 3.13, 512 4.84, 1024 6.86, 2048 8.47, 4096 8.08, 8192 2.00. Quicksort, random pivot: 16 0.095, 32 0.125, 64 0.211, 128 0.360, 256 0.607, 512 1.01, 1024 1.57, 2048 2.40, 4096 2.60, 8192 2.00. Family, 16 cells: 16 12.79, 32 2.30, 64 4.22, 128 8.29, 256 16.39, 512 32.49, 1024 64.61, 2048 128.84, 4096 257.50, 8192 515.00. Family, 64 cells: 16 3.77, 32 1.87, 64 2.62, 128 2.88, 256 4.64, 512 8.43, 1024 16.39, 2048 32.60, 4096 65.14, 8192 130.27. Family, 256 cells: 16 2.39, 32 3.42, 64 6.50, 128 10.65, 256 6.27, 512 2.47, 1024 4.36, 2048 8.58, 4096 17.08, 8192 34.13. Least over the grid, reads only and charged: heapsort 0.259 at 16 and 0.464 at 16; quicksort, random pivot 0.049 at 16 and 0.095 at 16; family, 16 cells 2.183 at 32 and 2.298 at 32; family, 64 cells 1.288 at 32 and 1.866 at 32; family, 256 cells 1.576 at 16 and 2.388 at 16. Both axes are logarithmic.1664256102440960.1110100fast memory M, cellstransfers × fast memory, per n²/Bheapsortquicksort, random pivotfamily, 16 cellsfamily, 64 cellsfamily, 256 cellsn = 8,192, write-backs chargedn²/B at one
Fig. 5 Transfers with write-backs times fast memory, over n2/Bn^2/B, against fast memory, n = 8,192. Quicksort, random pivot: 0.095 at 16 cells, 0.211 at 64, 1.01 at 512, 2.60 at 4,096. Heapsort: 0.464, 1.44, 4.84, 8.08. The family with 64 cells: 1.87 at 32 cells, its least, then 2.62, 2.88 and up; with 16: 2.30 at 32 cells; with 256: 2.39 at 16 cells and 2.47 at 512. On reads alone the least values were 0.049, 0.259, 1.29, 2.18 and 1.58.

Charged for its write-backs, quicksort’s least transfers times fast memory rises from 0.049 of n2/Bn^2/B to 0.095, at the same 16 cells, and the family’s least, over all three buffers, rises from 1.29 to 1.87. The first is the prediction exactly: about 0.1. The second is the prediction’s one outright failure. The family’s transfers were supposed not to move, and at its best point they move by 45%.

The best point is where the move is. The family’s least memory product was always found just below its buffer, 32 cells of fast memory holding the end of a 64-cell buffer, because transfers times MM rewards the smallest MM that does not fall off the cliff. That is exactly the configuration in which the buffer does not fit and its insertions dirty lines that are then evicted. On the read-only account those lines were free to throw away; on the honest one each costs a transfer. So the family’s floor on this product was partly an artefact of not charging the family for writing its own buffer.

The separation survives. The family’s least is still twenty times quicksort’s, against twenty-six before, and the earlier page’s argument for why the family cannot go below n2/Bn^2/B on this product is untouched: a sort that does not write into its input carries its progress between scans only in what it holds fast, so it needs about n/Mn/M scans. Quicksort’s escape from that argument was storing its progress in slow memory, and the repricing says what that storage costs: one write-back for about nine of every ten lines it reads, which is a factor under two on a product it beats the family on by twenty.

The family’s buffer was never free

The read-only family writes in two places, and only one of them is cheap: its output costs one more read of the array at any memory, but a buffer of 256 that does not fit writes back 85% of the lines it brings in at 128 cells, 369.2 reads of the array becoming 681.4The read-only family's block transfers in reads of the whole array, reads only (pale) and with write-backs (solid), against fast memory, n = 8,192, lines of 8. Buffer of 16: 16 cells 6460.6 → 6548.1, 32 cells 558.7 → 588.2, 64 cells 526.8 → 540.8, 128 cells 521.4 → 530.7, 256 cells 518.3 → 524.6, 512 cells 516.0 → 519.9, 1024 cells 514.4 → 516.9, 2048 cells 513.7 → 515.3, 4096 cells 513.5 → 515.0, 8192 cells 513.5 → 515.0. Buffer of 64: 16 cells 1756.6 → 1931.3, 32 cells 329.8 → 477.7, 64 cells 239.5 → 335.2, 128 cells 156.7 → 184.4, 256 cells 138.4 → 148.6, 512 cells 131.4 → 134.9, 1024 cells 129.6 → 131.1, 2048 cells 129.2 → 130.4, 4096 cells 129.1 → 130.3, 8192 cells 129.1 → 130.3. Buffer of 256: 16 cells 807.0 → 1222.4, 32 cells 477.4 → 874.4, 64 cells 451.4 → 831.6, 128 cells 369.2 → 681.4, 256 cells 118.0 → 200.6, 512 cells 35.9 → 39.5, 1024 cells 33.4 → 34.9, 2048 cells 33.2 → 34.3, 4096 cells 33.1 → 34.2, 8192 cells 33.1 → 34.1. Vertical rules mark each buffer. Both axes are logarithmic.1664256102440961010010³fast memory M, cellstransfers, in reads of the whole arraybuffer of 16buffer of 64buffer of 256n = 8,192; rules: each bufferpale: the reads alone
Fig. 6 The read-only family’s transfers in reads of the whole array, reads only (pale) and with write-backs (solid), n = 8,192. Buffer of 16: 558.7 → 588.2 at 32 cells, 516.0 → 519.9 at 512, 513.5 → 515.0 at 8,192. Buffer of 64: 329.8 → 477.7 at 32 cells, 156.7 → 184.4 at 128, 129.1 → 130.3 at 8,192. Buffer of 256: 369.2 → 681.4 at 128 cells, 118.0 → 200.6 at 256, 33.1 → 34.1 at 8,192. The vertical rules mark each buffer.

Where its buffer fits, the family’s write-backs are its output and almost nothing else: a buffer of 256 pays 34.1 reads of the array where it paid 33.1, at every memory from 1,024 cells. Where the buffer does not fit, a buffer of 256 at 128 cells pays 681.4 reads where it paid 369.2. The prediction that the family would not move was a prediction about the first regime, and there it is right to within one read of the array. Its output is written in order, so each output line is fetched once, filled, and written back once, and that is the whole of the change.

Below the buffer the family has a second written structure, and it is a bad one to write. An insertion into a sorted buffer shifts every larger entry one place, so a new value near the front of a 256-cell buffer dirties all thirty-two of its lines. While the buffer fits, those lines stay in fast memory and are written back once, at the end. When it does not, the scan’s input lines push the buffer’s lines out between insertions and every shift sends a line back. The earlier page found the buffer’s working set was mostly its end, which is why a buffer that did not fit was still cheap to read. Its write set is not its end: every insertion writes from the insertion point to the end, and on random keys early in a scan the insertion point is anywhere.

So the honest account adds one condition to the family’s claim on fast memory. The family needs its buffer to fit, not merely its buffer’s end, and its advantage is cleanest at the memory where that happens rather than just below it. The writes nobody counted found a B-tree writing forty-nine elements’ worth of blocks for every key it stored because each update rewrote a whole node; a sorted buffer that does not fit is the same structure at a smaller scale, rewriting its tail to place one value.

The gap grows with n

The gap between the two kinds of sort widens with n whatever is charged: quicksort's least transfers times fast memory falls from 0.301 of n²/B at n = 2,048 to 0.095 at 8,192, falling by nearly half with each doubling as the reads alone did, while the family's best stays between 1.87 and 2.68The least, over memories from 16 cells to n, of block transfers times fast memory over n²/B, at n = 2,048, 4,096, 8,192, lines of 8, LRU. n = 2,048: quicksort with a random pivot 0.158 reads only, 0.301 charged; heapsort 1.496 charged; the family's best buffer 2.37 reads only (16 cells), 2.68 charged (16 cells, at 32 cells of memory). n = 4,096: quicksort with a random pivot 0.089 reads only, 0.174 charged; heapsort 0.839 charged; the family's best buffer 1.89 reads only (64 cells), 2.46 charged (16 cells, at 32 cells of memory). n = 8,192: quicksort with a random pivot 0.049 reads only, 0.095 charged; heapsort 0.464 charged; the family's best buffer 1.29 reads only (64 cells), 1.87 charged (64 cells, at 32 cells of memory). Both axes are logarithmic.2,0484,0968,1920.11nleast transfers × fast memory, per n²/Bfamily, best buffer, chargedfamily, best buffer, readsheapsort, chargedquicksort, chargedquicksort, readsleast over the memory gridn²/B at one
Fig. 7 The least transfers times fast memory over memories from 16 cells to n, over n2/Bn^2/B, at n = 2,048, 4,096 and 8,192. Quicksort, random pivot, charged: 0.301, 0.174, 0.095; reads only: 0.158, 0.089, 0.049. Heapsort, charged: 1.50, 0.84, 0.46. The family’s best buffer, charged: 2.68, 2.46, 1.87; reads only: 2.37, 1.89, 1.29.

Quicksort’s least memory product, write-backs charged, falls from 0.301 of n2/Bn^2/B at nn = 2,048 to 0.174 at 4,096 and 0.095 at 8,192, nearly halving with each doubling of nn exactly as it did on reads alone, while the family’s best stays between 1.87 and 2.68. The ratio between them grows from nine to twenty over a factor of four in nn, and charging the write-backs changes the level of quicksort’s curve and not its slope.

The slope has a plain reason. Quicksort’s least product is always at the smallest memory, 16 cells, where its transfers are n/Bn/B times a number of passes that grows as log⁡2(n/M)\log_2(n/M), the levels of its recursion that do not fit, and its writes nearly double that number without changing how it grows. Transfers times MM over n2/Bn^2/B is then proportional to Mlog⁡2(n/M)/nM\log_2(n/M)/n, which at fixed MM falls nearly as 1/n1/n: from 2,048 to 8,192 that expression falls by a factor of 3.1, and the measured product by 3.2. The family’s least can never go below the floor, by the scanning argument, at any nn. So the in-place sort’s advantage on this product is not a constant to be traded against write costs; it grows with the input, and a factor of two in the cost of storing progress is small against it at any size measured here.

That puts the two products where the earlier page left them, with one correction. On transfers times writable cells, the in-place sorts were the read-only family’s equals given enough fast memory, and on the honest account they are not: their floor is twice the family’s, because a rearranged array is written once in full. On transfers times fast memory, the in-place sorts are better than any read-only sort by a margin that grows with nn, and the honest account narrows that margin by a factor under two at every size. The model a bound was quoted in drew the same moral about streaming summaries: change what the model charges and the verdict between two structures can move by a constant, while the verdict that grows with the input survives it.

What the replay still assumes

Write-allocate on one level. A miss on a write fetches the line before writing it. The family’s output is written in order and never read, and a cache with streaming stores would write those lines without fetching them, saving the family one read of the array. Nothing here measures that, and it would change the family’s numbers by the same few per cent its output costs it now.

Writes priced as reads. A write-back is charged one transfer, the same as a fetch. On disks and on most memories that is close to the truth. On flash and on the non-volatile memories that keep data through a power cut, a write can cost several times a read and wear the medium as well, and none of the products here would rank the sorts the same way.

Random keys, one seed a length. On random keys nearly every line holds a key out of place, so nearly every line an in-place sort reads it also writes. On input that is nearly sorted, a partition swaps little and the share of lines written back would fall; in-place is a claim has already shown how far a sort’s behaviour on such input can move from its behaviour on random keys.

Still open: a buffer that does not shift its tail

The family’s one real loss on the honest account is its buffer. A sorted array of the smallest values seen so far takes each new value by shifting every larger entry one place, so an insertion near the front writes every line from there to the end, and below the memory that holds the whole buffer each of those lines is evicted dirty and sent back. Its least memory product, found at exactly such a memory, rose by 45% for that reason alone, while its output cost it one read of the array.

A buffer does not have to be sorted while it fills. Kept as a max-heap of the smallest values seen, it compares each input value with its root, as the sorted buffer compares with its last entry, and an insertion replaces the root and sifts the new value down one path of about log⁡2S\log_2 S cells, rather than shifting a tail of up to SS. The heap is sorted once, in place, when the scan ends, which writes every line of the buffer once a scan, as the sorted buffer’s emptying already reads them. The measurement that follows gives the family that buffer, at the same three sizes, and replays it with write-backs charged at every memory. The prediction is that below its buffer the heap writes back about a third of the lines it brings in rather than 45% to 85%, since a sift-down’s path crosses about three of a 64-cell buffer’s eight lines where a shift crosses half of them on average, and that the family’s least memory product falls from 1.87 back towards the 1.29 it measured on reads alone. It could fail on the heap’s reads. A sift-down reads two children a level and jumps between lines a sorted buffer reads in order, so a heap that does not fit may fetch more lines than the sorted buffer did, and lose on misses what it saves on write-backs. The question is whether the read-only family’s best point on this product was a property of reading its input only, or of the particular structure it kept its buffer in.

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.

Block transferCost modelExternal-memoryIn placeLocalityLower boundTime space tradeoffWorking set