What a bound is

A product paid for in blocks

The published floor under sorting puts a bound on time times space, and a read-only family that scans its input over and over meets it, while every in-place sort sits far above it. Counted in block transfers rather than reads, the floor itself moves to n²/B: the family meets it at every block size, and so does every sort once memory holds the whole array. With a memory of 512 cells, quicksort sits a fixed eight times above the family at every block size, because partitioning is itself a scan. Only heapsort's distance grows with the block, from ten times at a line of one to 285 times at 64. The prediction that the family's lead would widen towards B holds against heapsort and nothing else.

A floor under a product measured the published lower bound on sorting that constrains time and space together: T⋅S=Ω(n2)T \cdot S = \Omega(n^2) in a model where every read costs one, and where space counts every cell an algorithm can write, including the array it sorts in place. It built the family that meets the bound. The family never writes its input: it keeps the SS smallest keys not yet output in a buffer of SS cells, scans the whole input to fill it, emits them, and scans again, ⌈n/S⌉\lceil n/S \rceil times. Its product came out at exactly n2(1+3/S)n^2(1 + 3/S), and every in-place sort, counted the way the bound counts, sat far above it. Quicksort’s comparisons times its nn writable cells were about fifteen times the family’s product.

Its closing section noticed something the bound says nothing about. The family’s reads are sequential — it runs through the input from one end to the other, time after time — and the sorts it was compared with are not. One access, eight kilobytes priced that difference: the same 65,536 accesses cost 1,024 block transfers in one order and 65,536 in another. So in transfers rather than reads, the family’s time should fall by a factor of the block size BB, and heapsort’s, which sprays its accesses across the heap, should not. The section predicted that the family’s advantage would widen from fifteenfold towards BB. It also predicted that the order among the sorts would change, since the three quicksorts sit within a factor of 1.1 of each other in reads and differ in locality.

The replay confirms half of that and moves the floor.

Replaying the sorts through a block memory

Every sort is run on 8,192 keys in random order with its accesses recorded — each read and write of the array, in order — and the trace is replayed through the memory model the transfer pages use: a fully associative memory of MM cells in lines of BB elements, least recently used replaced. A transfer is a line fetched. The sorts are heapsort, quicksort with three pivot rules, and Shellsort, all of which work inside the array. Merge sort is left out because its buffer’s accesses are not in the traces it records, so its transfers would be undercounted. Space is what the bound counts: nn writable cells plus whatever a sort allocates, and for the family its buffer plus three cells of bookkeeping. The family’s scans are sequential by construction, ⌈n/B⌉\lceil n/B \rceil transfers each, and its output is written once in order.

Counting every access as one — every read and write, where the earlier page counted comparisons — puts quicksort with a random pivot at 43 n2n^2, heapsort at 94, Shellsort at 60, and the family at 1.04. Every sort is dearer by this count than by comparisons, because each comparison comes with reads and each swap with writes.

The product in block transfers at n = 8,192 and a memory of 512 cells: the read-only family's falls as 1/B, from 1.04 n² at B = 1 to 0.016 at 64; quicksort's falls alongside it, 8.2 times the family's at B = 8 and 8.4 at 64; heapsort's stops falling at 4.59 n², 285 times the family's by B = 64Block transfers times cells of writable space, as a multiple of n², against the line size B in array elements, for each sort replayed through an LRU memory of 512 cells at n = 8,192 on random input. Heapsort: B 1 10.96, B 2 9.51, B 4 6.73, B 8 5.32, B 16 4.59, B 32 4.27, B 64 4.63. Shellsort: B 1 14.28, B 2 9.17, B 4 5.33, B 8 2.96, B 16 1.61, B 32 0.87, B 64 1.85. Quicksort, random pivot: B 1 8.52, B 2 4.26, B 4 2.13, B 8 1.07, B 16 0.53, B 32 0.27, B 64 0.14. Quicksort, first pivot: B 1 8.70, B 2 4.35, B 4 2.18, B 8 1.09, B 16 0.55, B 32 0.27, B 64 0.14. The read-only family: B 1 1.04, B 2 0.52, B 4 0.26, B 8 0.13, B 16 0.065, B 32 0.032, B 64 0.016. Quicksort with a median-of-three pivot lies within 3% of the other two at every B and is not drawn. Both axes are logarithmic.12481632640.1110line size B, array elementstransfers × cells, per n²heapsortShellsortquicksort, random pivotquicksort, first pivotthe read-only familyn = 8,192, memory 512 cells, LRUrandom input
Fig. 1 Transfers times cells, per n2n^2, against the line size, n = 8,192, memory of 512 cells. The family: 1.04 at B = 1, 0.13 at 8, 0.016 at 64. Quicksort, random pivot: 8.52, 1.07, 0.14. Heapsort: 10.96, 5.32, 4.63. Shellsort: 14.28, 2.96, 1.85.

In transfers, the family’s product falls as 1/B1/B — 1.04 n2n^2 at a line of one element, 0.13 at eight, 0.016 at 64 — and quicksort’s falls alongside it, 8.2 times the family’s at B=8B = 8 and 8.4 times at 64. The prediction had quicksort’s accesses as the scattered kind. They are not. A partition is a scan: two pointers walking towards each other through a contiguous range, every element of which is read once. Each line a partition fetches serves BB consecutive reads, exactly as the family’s scans do. So quicksort’s transfers shrink with BB at the same rate, and the ratio between the two stays where it was.

Heapsort is the case the prediction was built on, and it behaves as predicted. Its product falls from 10.96 n2n^2 at a line of one to 5.32 at eight and then stops, 4.59 at sixteen and 4.63 at 64. By B=64B = 64 it is 285 times the family’s. Where an algorithm looks drew heapsort’s access pattern as a spray between the root and the leaves. A longer line brings in the neighbours of a heap node, which a sift-down will not visit again until it happens back that way.

What a line brings in, sort by sort

Why the curves part: a block transfer serves 1/B of the family's reads at every B, and about 318 of quicksort's at B = 64; heapsort's accesses cost 0.049 transfers each at B = 16 and 0.049 at 64 — past sixteen elements a line, a longer line brings heapsort nothing it usesBlock transfers per memory access (reads and writes of the array), against the line size B, LRU memory of 512 cells, n = 8,192. Heapsort: B 1 0.116, B 2 0.101, B 4 0.071, B 8 0.056, B 16 0.049, B 32 0.045, B 64 0.049. Shellsort: B 1 0.239, B 2 0.154, B 4 0.089, B 8 0.050, B 16 0.027, B 32 0.015, B 64 0.031. Quicksort, random pivot: B 1 0.196, B 2 0.098, B 4 0.049, B 8 0.025, B 16 0.012, B 32 0.006, B 64 0.003. Quicksort, first pivot: B 1 0.191, B 2 0.095, B 4 0.048, B 8 0.024, B 16 0.012, B 32 0.006, B 64 0.003. The read-only family: B 1 1.000, B 2 0.500, B 4 0.250, B 8 0.125, B 16 0.063, B 32 0.031, B 64 0.016. Both axes are logarithmic.12481632640.010.11line size B, array elementstransfers per accessheapsortShellsortquicksort, random pivotquicksort, first pivotthe read-only familyn = 8,192, memory 512 cellsreads and writes of the array
Fig. 2 Transfers per access against the line size, memory of 512 cells. The family: exactly 1/B. Quicksort: 0.196 at B = 1, 0.025 at 8, 0.003 at 64. Heapsort: 0.116, 0.056, then 0.049 at 16 and at 64. Shellsort: 0.239, 0.050, 0.015 at 32 and 0.031 at 64.

The same numbers divided by the accesses show what locality each sort has. The family pays exactly 1/B1/B a read at every BB: it uses every element of every line once and never returns. Quicksort pays less than that. At B=1B = 1 its accesses already miss only a fifth of the time, because once a range is smaller than the memory, it is partitioned to the end without fetching anything twice. At B=64B = 64 a transfer serves over three hundred of its accesses. Heapsort’s cost a transfer falls to 0.049 by B=16B = 16 and stays there. Past sixteen elements a line, a longer line brings heapsort nothing it uses before the line is evicted.

Shellsort does something the others do not: its cost rises again at the longest line, from 0.015 transfers an access at B=32B = 32 to 0.031 at 64. With 512 cells of memory in lines of 64 there are only eight lines. A pass of Shellsort with a large gap compares elements a gap apart, and when the gap spans more lines than the memory holds, every comparison fetches. The longest line gives the least room for anything else, and Shellsort is the one sort here that needs room for several distant places at once.

The floor moves with the block

The first plate hides the most important fact on this page, because it holds the memory fixed.

With a memory that holds the array, every sort's product in transfers is n²/B — 0.125 n² for heapsort at n = 8,192 and B = 8, against the family's 0.130 — because each line is fetched once; the sorts' excess over the family is entirely what a smaller memory makes them fetch againBlock transfers times cells of writable space, per n², against the memory M in cells, at B = 8 and n = 8,192, random input. Heapsort: M 128 8.409, M 256 6.913, M 512 5.325, M 1,024 3.753, M 2,048 2.302, M 4,096 1.085, M 8,192 0.125. Shellsort: M 128 3.890, M 256 3.595, M 512 2.963, M 1,024 2.355, M 2,048 2.107, M 4,096 1.487, M 8,192 0.125. Quicksort, random pivot: M 128 1.514, M 256 1.282, M 512 1.067, M 1,024 0.831, M 2,048 0.636, M 4,096 0.334, M 8,192 0.125. Quicksort, first pivot: M 128 1.612, M 256 1.339, M 512 1.089, M 1,024 0.910, M 2,048 0.634, M 4,096 0.434, M 8,192 0.126. The read-only family: M 128 0.130, M 256 0.130, M 512 0.130, M 1,024 0.130, M 2,048 0.130, M 4,096 0.130, M 8,192 0.130. The dashed line is n²/B. The family keeps its 128 cells of buffer at every M. Both axes are logarithmic.1282565121,0242,0484,0968,1920.1110memory M, cellstransfers × cells, per n²heapsortShellsortquicksort, random pivotquicksort, first pivotthe read-only familyn = 8,192, B = 8dashed: n²/B
Fig. 3 Transfers times cells, per n2n^2, against the memory, n = 8,192, B = 8. At 128 cells: heapsort 8.41, Shellsort 3.89, quicksort 1.51, the family 0.13. At 8,192 cells, a memory that holds the array: every sort 0.125, the family 0.130. The dashed line is n2/Bn^2/B.

With a memory that holds the whole array, every sort’s product in transfers is exactly n2/Bn^2/B — 0.125 n2n^2 at B=8B = 8 — against the family’s 0.130. Each line of the array is fetched once and never evicted, so the sort costs n/Bn/B transfers whatever it does, and its nn writable cells make the product n2/Bn^2/B. The family’s product is (n/S)(n/B)(S+3)(n/S)(n/B)(S + 3), which is n2/Bn^2/B plus its bookkeeping. So in transfers the floor is not n2n^2 but n2/Bn^2/B. The counting floor under comparison sorting, which the floor under every comparison sort derived, does not move with the block at all, since a comparison is not a transfer; this floor does, because it is a floor on reading. Every algorithm on the page can reach it, the family with SS cells of memory and the sorts with nn.

That changes what the family’s advantage is. In reads it was an advantage in the product itself: an in-place sort could not get under fifteen times the floor. In transfers the product alone does not separate them, because a sort whose memory holds its input is on the floor. What separates them is the memory each needs to get there. The family reaches n2/Bn^2/B holding 131 cells. Quicksort needs the whole array in memory to reach it. At 512 cells it is eight times above, and at 2,048 cells five times. The advantage the bound measured in reads becomes, in transfers, an advantage in how much fast memory each algorithm needs to reach the floor. That is a different resource from the SS in the product, and the product has no term for it.

The product also hides which algorithm is faster. At B=8B = 8 and 512 cells, the family makes 66,560 transfers to sort 8,192 keys: 64 scans of the array at 1,024 transfers each, and one more to write the output. Quicksort with a random pivot makes 8,706, heapsort 43,615. By time alone, counted in transfers, quicksort is seven and a half times faster than the family and five times faster than heapsort. The family’s small product comes entirely from its small space; its time is the largest on the page. A bound on T⋅ST \cdot S allows exactly that trade, and the family is the algorithm that takes it to its end. A machine with memory to spare would never run it, and a machine with 131 cells would have no other choice.

Eight times at one size, and growing

How far above the family each sort sits in transfers, B = 8 and a memory of 512 cells: quicksort from 2.9 times at n = 1,024 to 13.9 at 32,768, close to 2·log₂(n/M) + 1 — the partitioning levels whose ranges do not fit in memory, each a scan — and heapsort from 7.7 to 65.1Each sort's product in block transfers divided by the read-only family's, against n, B = 8, LRU memory of 512 cells, random input. Heapsort: 1,024 7.7, 2,048 16.8, 4,096 28.5, 8,192 41.0, 16,384 53.1, 32,768 65.1. Shellsort: 1,024 8.8, 2,048 12.9, 4,096 17.2, 8,192 22.8, 16,384 31.2, 32,768 46.6. Quicksort, random pivot: 1,024 2.9, 2,048 4.6, 4,096 6.4, 8,192 8.2, 16,384 11.8, 32,768 13.9. Quicksort, first pivot: 1,024 3.1, 2,048 5.0, 4,096 6.9, 8,192 8.4, 16,384 9.6, 32,768 12.7. The dashed line is 2·log₂(n/M) + 1: 1,024 3.0, 2,048 5.0, 4,096 7.0, 8,192 9.0, 16,384 11.0, 32,768 13.0. Both axes are logarithmic.1,0242,0484,0968,19216,38432,76825102050keys sorted, nproduct over the family'sheapsortShellsortquicksort, random pivotquicksort, first pivot2·log₂(n/M) + 1B = 8, memory 512 cellsdashed: 2·log₂(n/M) + 1
Fig. 4 Each sort’s product in transfers divided by the family’s, against n, B = 8, memory of 512 cells. Quicksort, random pivot: 2.9 at n = 1,024, 8.2 at 8,192, 13.9 at 32,768. Heapsort: 7.7, 41.0, 65.1. Shellsort: 8.8, 22.8, 46.6. Dashed: 2·log₂(n/M) + 1.

Quicksort’s distance from the family grows with nn, from 2.9 times at 1,024 keys to 13.9 at 32,768, and it grows close to 2log⁡2(n/M)+12\log_2(n/M) + 1. Each level of partitioning whose ranges are larger than the memory is a full scan of the array, n/Bn/B transfers. There are about log⁡2(n/M)\log_2(n/M) such levels on random input, and each level’s scan both reads and writes. The family pays one scan for each of its ⌈n/S⌉\lceil n/S \rceil buffers’ worth, so its product is fixed near n2/Bn^2/B while quicksort’s grows by a scan a level. Heapsort’s distance grows much faster, from 7.7 times to 65.1. Its transfers are a fraction of its accesses that does not shrink as the heap outgrows the memory.

A transfer for each level that does not fit

The two growth rates have simple accounts, and the traces confirm both. Quicksort’s transfers, divided by the n/Bn/B of one scan of the array, come to 3.3 scans at 1,024 keys, 8.5 at 8,192 and 14.3 at 32,768. That is close to two for each level of partitioning whose range exceeds the memory, log⁡2(n/M)\log_2(n/M) of them, plus the scan that reads the array in. Each such level reads its ranges in order and writes them back in order, and nothing below the memory’s size costs anything further.

Heapsort’s transfers, divided by the number of keys, come to 1.1 at 1,024 keys, 5.3 at 8,192 and 8.4 at 32,768 — between 1.1 and 1.4 for each level of the heap below the memory’s size. A sift-down walks from the root to a leaf, one node a level. The top levels of the heap are few enough nodes to stay in memory; below them each step lands on a node no recent step has touched, and costs a line. A line of eight holds a node’s seven neighbours at its own level, and the sift-down never wants them. A tree with nodes the size of a block found the remedy for search: make a node as wide as a transfer, so that one line carries a whole step’s choices. A heap whose nodes had BB children would cut the levels below memory by a factor of log⁡2B\log_2 B. Heapsort as written has two children a node, and pays a line for each of its levels.

So both products grow with the same quantity, the number of levels that do not fit. Quicksort pays a scan of the array for each level, n/Bn/B transfers, and heapsort pays a transfer per key for each, nn transfers. The ratio between them is the block size, which is why heapsort falls behind as the line grows and quicksort does not.

Whether the order changes

The order of the five sorts by their product: counting every access, and in transfers with lines of 1, 8 and 64 at a memory of 512 cells — the three quicksorts stay together at the top of every column, and heapsort and Shellsort change places between an unblocked memory and a blocked oneRank from least product (top) to most, n = 8,192, random input. every access: quicksort, random pivot 43.42, quicksort, first pivot 45.60, quicksort, median of three 47.56, Shellsort 59.64, heapsort 94.30. B = 1: quicksort, random pivot 8.52, quicksort, first pivot 8.70, quicksort, median of three 8.76, heapsort 10.96, Shellsort 14.28. B = 8: quicksort, random pivot 1.07, quicksort, first pivot 1.09, quicksort, median of three 1.10, Shellsort 2.96, heapsort 5.32. B = 64: quicksort, random pivot 0.136, quicksort, first pivot 0.139, quicksort, median of three 0.141, Shellsort 1.85, heapsort 4.63.every accessB = 1B = 8B = 64heapsortShellsortquicksort, median of threequicksort, first pivotquicksort, random pivotheapsortShellsortmedian of threefirst pivotrandom pivotn = 8,192, memory 512 cellstop: the least product
Fig. 5 The five sorts ranked by their product, least at the top, n = 8,192, memory of 512 cells. Counting every access: the three quicksorts, then Shellsort, then heapsort. In transfers at B = 1: the quicksorts, then heapsort, then Shellsort. At B = 8 and 64: the quicksorts, Shellsort, heapsort.

The three quicksorts stay together at the top of every column, within 3% of one another: their pivot rules change which elements are compared, not the order in which memory is walked. The prediction that locality would separate them fails. All three walk their ranges the same way, and a pivot rule changes the split points, which move the transfers by a few per cent. The random pivot is cheapest in every column, as it is in reads.

The order does change lower down. Counting every access, Shellsort’s product is well under heapsort’s, 60 against 94. In transfers with lines of one element, where only the memory’s reuse counts and not the block, heapsort’s is lower, 10.96 against 14.28. That is consistent with heapsort’s upper levels being small enough to stay in memory while Shellsort’s widely spaced passes are not. With lines of eight or more, Shellsort is ahead again, because its passes with small gaps are close to scans and gain from blocks the way quicksort’s partitions do. The count is not the time found the same reordering among sorts timed on a real machine; here it follows from a model with two parameters, MM and BB, and it happens between their settings.

What the replay rests on

One memory model. Fully associative, least recently used, one level, no prefetching. A real machine has several levels, and a hardware prefetcher would make every scan cheaper still — the family’s and quicksort’s — and would do little for heapsort. The ratio between the scans would not move. The sprayed accesses would lose further ground.

Space as the bound counts it. The product charges a sort nn cells for the array it overwrites and the family its buffer. The memory MM is a property of the machine and is not charged to anyone. Charging it as well would add MM to every sort’s space and M−SM - S to the family’s, and it would stop the sorts reaching the floor: their space would be at least 2n2n once MM is large enough to hold the array.

Fetches, not write-backs. A transfer here is a line brought into memory. A real memory must also write a changed line back when it is evicted, and every in-place sort changes most of the lines it fetches, while the family changes none of its input. Counting write-backs would add up to a second transfer for most of each sort’s fetches and nothing to the family’s scans, so it would widen every gap on this page, by at most a factor of two. It was not replayed.

Random input, one seed. Every trace is one run of 8,192 random keys; the pattern of accesses depends on the keys for the quicksorts and heapsort, and a sorted or reversed input would move quicksort with a first-element pivot a long way. What a pass costs when it is a file found the same economy for a streaming selection on a file. A pass is a read of every block, and it is cheap only when the blocks are read in order.

A floor measured, not proved. That every algorithm here sits at or above n2/Bn^2/B is a measurement on these sorts, of the kind a floor that holds if something else does was careful to label: a claim with evidence and no proof. The published bound is for unit-cost reads, and a proof of T⋅S=Ω(n2/B)T \cdot S = \Omega(n^2/B) in transfers would need its own argument. The measurement says only that nothing here comes under it, and that the family and every sort with a large enough memory come within 4% of it.

Still open: charging the memory the product leaves out

The product failed to separate the family from quicksort in transfers because it charges quicksort for nn cells it writes and not for the MM cells of fast memory it needs to write them cheaply. The family needs almost no fast memory at all. A three-way account — transfers, writable cells and fast memory — would say what the two-way product cannot: that the family is on the floor with 131 cells of fast memory, and quicksort needs 8,192 to join it.

The measurement that follows sweeps both MM and the family’s buffer SS, and draws, for each algorithm, the least fast memory at which its product in transfers comes within a stated factor of n2/Bn^2/B. The prediction is that the family’s need is its own buffer, a few hundred cells whatever nn is. Quicksort’s should grow as n/2cn / 2^{c} for a stated factor 2c2^c above the floor, since each halving of the memory adds a level of full scans. Heapsort’s should be all of nn, since no memory smaller than its heap keeps it within a constant of the floor. If that holds, the resource the floor under a product was implicitly trading is not space in general but writable space against fast memory. A sort that writes nothing in place needs almost no fast memory to be on the floor; a sort that writes everything in place needs all of it.

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 modelGuaranteeHonest limitLocalityLower boundModelSpace time tradeTrade off