Structures

Two lines a superblock

Laid out as ten separate arrays, the succinct range minimum's query touched 17.9 cache lines at 32,768 values. Regrouped as two lines a superblock — one holding everything indexed by position, the other everything indexed by the superblock — it touches 12.5, under the segment tree's 19.8, against a prediction of about 8. The regrouping undercuts the tree on isolated queries from 8,192 values up and on small caches, holds less content than the separate arrays and occupies more, 15.2 bits a value against 12.1. And it barely helps the document listing, whose crossing moves from 12,143 occurrences to 11,274: queries that arrive together in one window share a compact array's lines, and a record's lines they do not.

The lines a constant-time query lands in priced the succinct range minimum in the unit a machine bills — distinct 64-byte lines — and found its advantage over the segment tree nearly gone. Counted in reads, a query cost the succinct structure 16.9 against the tree’s 41.7 at 32,768 values. Counted in lines, it cost 17.9 against 19.8, and below about twenty thousand values the tree touched fewer. The reason was a plate of ten bars: the succinct structure is assembled from ten arrays — select samples and offsets, a rank directory, the parentheses, a shared table of in-block answers, two levels of minima and two levels of sparse tables, and the caller’s values — and its query reads nearly every one of them, each in a different place. The tree is one array whose top levels share a line.

That essay ended by proposing the obvious repair. Most of those arrays are indexed by the same thing: a position in the parentheses, which belongs to a block and a superblock. Put everything a query reads about one superblock into the same place and the ten arrays become a few records. It predicted that a query would fall from about 17.9 lines to about 8, and that the document listing’s crossing, which pricing in lines had pushed out to 12,143 occurrences, would come back under the tree’s 7,952, near 5,000. It named the way it could fail: padding.

What goes in a record

The ±1 structure at the centre of the succinct range minimum cuts the parentheses into blocks of eight and groups seventeen blocks into a superblock, so a superblock covers 136 parentheses. Everything a query reads can be sorted by what it is indexed by.

Record A holds what is indexed by position inside the superblock: its 136 parentheses, the number of ones before it, and the minimum of each of its seventeen blocks — 306 bits, in one line. The count of ones replaces the rank directory entirely: a rank is that count plus a population count of the parentheses up to the position, both inside the line already fetched. It also replaces the select support’s offsets: a select now reads a sample naming the superblock its one falls in, and walks forward through the records’ counts to the superblock that holds it.

Record B holds what is indexed by the superblock itself: the sparse table over its seventeen blocks (59 entries of five bits), its own minimum and where it lies, and its column of the superblock sparse table — the entry at every level that starts at this superblock. That is 393 bits, in a second line.

Two things stay where they were. The table of in-block answers that the table that fits inside a block built is shared by every structure of the same block length and is indexed by a block’s shape, not its position, so it has no superblock to belong to. The caller’s values are the caller’s.

The structure is unchanged. The same blocks, the same tables and the same query rule answer every range, and the replay that prices each query is required to return the position the real structure returns; the only thing that differs between this layout and the previous one is where each entry the query reads is kept.

Twelve and a half, not eight

One record a superblock brings a succinct query from 17.9 lines to 12.5 at 32,768 values, under the tree's 19.8; it undercuts the tree from 8,192 values up, and at 512 the tree is still cheapest (6.9 against 10.9)200 random ranges at each size: distinct 64-byte lines a query touches. 512: tree 6.88, separate arrays 11.94, interleaved 10.94; 2,048: tree 11.24, separate arrays 14.38, interleaved 11.75; 8,192: tree 15.79, separate arrays 16.60, interleaved 12.34; 32,768: tree 19.84, separate arrays 17.88, interleaved 12.49. The two layouts read the same entries of the same structure and return the same positions; only where those entries sit differs.05101520values in the array (log scale)distinct lines a query touches5122,0488,19232,768segment treeseparate arraysinterleaved200 random ranges a sizelines of 512 bits
Fig. 1 Distinct lines a query touches at four array sizes, for the segment tree and for the succinct structure laid out both ways. The interleaved layout falls below the tree between 2,048 and 8,192 values and stays there.

At 32,768 values a query now touches 12.5 lines, against 17.9 for the separate arrays and 19.8 for the tree. That is a cut of 30%, and it is real, but the prediction was 55%. The interleaved layout passes under the tree between 2,048 and 8,192 values — at 8,192 it is 12.3 to the tree’s 15.8 — where the separate arrays never got clearly under it at any size measured. At 2,048 it is 11.8 to the tree’s 11.2, and at 512 it is 10.9 to 6.9: a small array still favours the structure whose whole top fits in a few lines.

The interleaved layout’s line count barely grows with the array — 10.9 to 12.5 across a sixty-fourfold growth — which is what a constant-time structure was promised to do in operations, now done in lines. What it does not do is reach the floor the prediction assumed.

Why a small array still belongs to the tree

The left-hand end of the plate is worth reading on its own, because it is where most range minima actually run. A document listing over a collection of a few dozen short documents asks its queries of an array of a few thousand values, and the structures there are tiny.

At 512 values the whole segment tree is 20 lines. Its root’s line holds the top five levels and part of the sixth, the next lines hold the rest, and a query touches 6.9 of the 20 — a third of the structure, read along two paths that share their first several nodes. The interleaved succinct structure at the same size is 55 lines, values included: nineteen superblocks of 55 parentheses at a block length of five, two lines each, a shared table of seven lines and a line of select samples. A query touches 10.9 of them.

The succinct structure’s fixed cost is the number of places it must look, and that number does not shrink with the array. Both ends of the range, the select samples, the shared table at both ends, the block tables and the value are six or seven places at 512 values just as at 32,768; only the sparse table over superblocks, with nineteen superblocks to choose among, is cheaper. The tree’s cost is its depth, and at 512 values its depth is nine, five of which are inside one line. So below a few thousand values the comparison is between a structure that pays per place and has six of them, and a structure that pays per level and has four levels that are not free. The crossing between those two descriptions is somewhere between 2,048 and 8,192 values, and nothing about the structure’s asymptotic description says which side of it a given index is on.

Where the twelve lines are

Laid out one record a superblock, a succinct query at 32,768 values touches 12.5 lines: 3.88 in record A, 3.88 in record B, 1.99 in the shared table — the two ends of each sparse-table lookup and the two in-block answers are reads no grouping by position can bring togetherDistinct 64-byte lines a query touches in each kind of line of the interleaved layout, averaged over 200 random ranges of 32,768 values: select samples 1.75, record A, at the ends 2.85, record A, a third superblock 1.02, record B, block tables 1.98, record B, superblock column 1.90, shared table 1.99, values 1.00; total 12.49. Record A holds a superblock's parentheses, the ones before it and its block minima; record B its block tables, its minimum and its column of the superblock table. The segment tree on the same queries: 19.84 lines.select samples1.75record A, at the ends2.85record A, a third superblock1.02record B, block tables1.98record B, superblock column1.90shared table1.99values1.00segment tree on the same queries: 19.84 linesinterleaved: 12.49 lines a query, by kind of line32,768 values, 200 rangeslines of 512 bits
Fig. 2 The interleaved layout’s lines by kind, at 32,768 values. Record A at the query’s two ends, record A of a third superblock, record B three or four times, the shared table twice, a select sample or two, and the value.

The prediction counted one line per superblock the query touches and then forgot how many superblocks a query touches. A range minimum over a wide range reads at least four of them.

The two ends account for 2.85 lines of record A: the superblock holding the range’s start and the one holding its end, each read for its parentheses, its count and its block minima — and occasionally a neighbour, when a select’s walk has to step forward. The middle costs record B 3.88 times: the block tables of the start’s and the end’s superblocks, and then the superblock sparse table, which answers “the smallest superblock between these two” by reading two entries — one in the column of the first superblock of the middle span and one in the column of a superblock a power of two before its end. Those two entries are in two different superblocks’ records by construction; that is how a sparse table works. The winner costs record A of a third superblock, 1.02 lines, because once the middle span has named its smallest superblock the query has to read that superblock’s block minima to find the block. And the shared table costs 1.99 lines, because the in-block answers at the two ends are two entries of a 162-line table indexed by the blocks’ shapes, which are unrelated.

So the floor is not one line per part but one line per place the query has to look, and a range minimum over a wide range looks in five or six places: both ends, two ends of a sparse-table lookup, the superblock it names, and the table. Grouping by position brings everything about one place into one line. It cannot bring two places together, and a sparse table is two places by design.

Two probes are two misses found the same floor for cuckoo hashing: two candidate slots that are independent by construction are two lines however each is laid out, and the bucket that fits a line bought its improvement by making each place hold more, not by making the places fewer.

A stream of queries

Interleaved, the succinct structure misses 12.7 lines a query with 8 lines cached against the separate arrays' 20.2 and the tree's 23.3, and 9.7 against 10.0 at 128 — but 6.0 against the tree's 5.0 at 512, where its padding makes it the largest of the three2,000 random ranges of 32,768 values replayed through a fully associative LRU of K lines: misses a query. K = 0: tree 46.08, separate 34.72, interleaved 29.67; K = 8: tree 23.28, separate 20.25, interleaved 12.66; K = 32: tree 14.49, separate 16.83, interleaved 11.54; K = 128: tree 9.97, separate 13.01, interleaved 9.70; K = 512: tree 5.00, separate 5.38, interleaved 6.04; K = 2048: tree 1.00, separate 0.85, interleaved 0.94. Footprints: tree 2,048 lines, separate 1,896, interleaved 2,095, each including the array of values.02040lines the cache holds (log scale; first point: none)misses a query, over a stream of queriesnone8321285122,048segment treeseparate arraysinterleaved32,768 values, LRU of linesmisses a query
Fig. 3 Misses a query over a stream of 2,000 random queries through an LRU cache of lines, for the tree and both layouts. The interleaved layout is best wherever the cache is small; when the cache is large enough to hold most of a structure, the smallest structure wins.

On a stream of random queries the interleaved layout’s advantage is largest where the cache is smallest. With eight lines cached it misses 12.7 lines a query, against 20.2 for the separate arrays and 23.3 for the tree; with 32 lines, 11.5 against 16.8 and 14.5. At 128 lines it has nearly lost its lead over the tree, 9.7 against 10.0, and at 512 it is the worst of the three, 6.0 against the tree’s 5.0 and the separate arrays’ 5.4.

The large-cache end is the price of the layout showing up as misses. A cache of 512 lines holds a quarter of each structure, and what it can keep resident depends on how much of what the query re-reads fits. The interleaved structure, padding included, is the largest of the three at 2,095 lines; the tree’s re-read part, its top levels, is compact. Across the middle of the plate the interleaved layout wins because each query needs fewer distinct lines, and at the right-hand end it loses because each line it keeps carries less of what the next query needs.

Queries that arrive together

The document listing does not ask random queries. It asks a range minimum over the pattern’s rows, then over the part left of the answer and the part right of it, recursively — so its queries are nested inside one range and land near one another. The next measurement isolates that: batches of sixteen random queries confined to a window of ww positions, each batch charged its distinct lines, for windows from 64 positions to the whole array.

When 16 queries share a window the tree touches the fewest lines at every width up to 4,096 (3.46 a query at 1,024, against 4.25 and 4.26); interleaving wins only when the window is the whole array (10.55 against 11.44), and inside a window it is no better than the separate arrays32,768 values; batches of 16 random ranges inside a random window of w positions, each batch charged its distinct lines, divided by its queries. w = 64: tree 0.97, separate 1.46, interleaved 1.31; w = 256: tree 1.64, separate 2.60, interleaved 2.43; w = 1,024: tree 3.46, separate 4.25, interleaved 4.26; w = 4,096: tree 6.37, separate 7.25, interleaved 7.30; w = 32,768: tree 11.44, separate 13.51, interleaved 10.55. A document listing's queries are the narrow-window case: nested inside one range of rows.0510width of the window a batch's queries fall in (log scale)distinct lines a query, charged per batch of 16642561,0244,09632,768separate arrayssegment treeinterleaved32,768 values, batches of 16lines shared within a batch
Fig. 4 Lines a query when sixteen queries fall in one window, charged per batch. The tree is cheapest in every window narrower than the array; the interleaved layout wins only when the window is everything.

In every window narrower than the array, the tree touches the fewest lines: 3.46 a query in a window of 1,024 positions, against 4.25 for the separate arrays and 4.26 interleaved; 1.64 against 2.60 and 2.43 in a window of 256. Only when the window is the whole array — sixteen unrelated queries — does the interleaved layout win, 10.55 against 11.44. Inside a window it is no better than the separate arrays, and at 1,024 positions it is a hundredth worse.

What changed is which sharing counts. An isolated query shares lines only with itself, and grouping by superblock makes it share more. A batch of queries in one window shares lines with the other queries in the batch, and here the separate arrays’ compactness is the asset: one line of the rank directory covers 2,560 parentheses, one line of select offsets covers forty-odd samples, and sixteen queries inside a thousand positions find most of what they need already fetched. A record covers 136 parentheses, which is 68 values, so a window of 1,024 values spans fifteen superblocks and sixteen queries inside it reach most of their records, each a line of its own. The tree shares best of all, because every query in the window descends through the same subtree.

The listing, priced again

Priced in 64-byte lines, interleaving the succinct structure moves the chain's crossing from 12,143 occurrences to 11,274, still behind the segment tree's 7,952: the scan reads 171 document entries to a lineDocument listing on 8 documents of 32,768 characters, the pattern planted in 3, answer 8 documents throughout. Distinct lines each listing touches, cold: the scan reads the document array over the pattern's rows at 3 bits an entry and marks a 8-bit bitmap; the chain walk reads what its range structure reads, the chain value and the document at each answer. 441 occurrences: scan 5, tree 27 (387 counted visits), interleaved 22 (110); 579 occurrences: scan 6, tree 28 (429 counted visits), interleaved 23 (109); 1,140 occurrences: scan 9, tree 33 (471 counted visits), interleaved 32 (136); 3,429 occurrences: scan 22, tree 43 (614 counted visits), interleaved 50 (177); 6,419 occurrences: scan 39, tree 46 (650 counted visits), interleaved 58 (180); 12,552 occurrences: scan 75, tree 54 (609 counted visits), interleaved 70 (163). With the separate arrays the succinct chain read 26, 29, 41, 57, 67, 73 lines at the same points. Crossing in lines: tree 7,952, succinct 11,274.10³10⁴5102050occurrences of the pattern (log scale)distinct lines a listing touches (log scale)scanchain, separate arrayschain, interleavedchain, segment treetree crosses at 7,952interleaved crosses at 11,274separate crosses at 12,1438 documents of 32,768lines of 512 bits
Fig. 5 The document listing on eight documents of 32,768 characters, priced in lines, with the succinct chain on both layouts and the tree’s chain. The interleaved chain is cheaper than the separate one at every point and still more expensive than the tree’s from about a thousand occurrences up.

So the listing barely moves. With the interleaved layout the succinct chain touches 22 lines at 441 occurrences and 70 at 12,552, against 26 and 73 with the separate arrays, and it crosses the scan at 11,274 occurrences — against 12,143 before, and still behind the tree’s 7,952. The second half of the prediction, a crossing near 5,000 and under the tree’s, failed outright.

The reason is the batch plate. A listing’s queries are a batch in a narrow window, and the narrower and more numerous they are the more the tree’s shared descent and the separate arrays’ compact directories are worth. The interleaved layout helps most where the listing is cheapest anyway, on its first few wide queries, and helps least on the many narrow ones that make up the rest. The scan, which the crossing that never arrives found already wins on most real collections, reads 170 document entries a line in every layout, and none of this touches it.

What the records cost

At 32,768 values the interleaved records hold 10.4 bits a value of content — less than the separate arrays' 12.0, since a count in each record replaces the rank directory and the select offsets — and occupy 15.2, because two lines a superblock carry 4.8 bits a value of padding; the tree is 32.0Bits a value, the shared in-block table (82,944 bits, one per block length) and the array of values excluded from all three. Segment tree: 32.00. Separate arrays: 12.02 of content, 12.08 with each array rounded to whole lines; parts select samples 0.25, select offsets 1.50, rank directory 0.38, parentheses 2.00, superblock minima 0.25, superblock table 1.05, block minima 2.25, block tables 4.34. Interleaved: 10.42 of content (record A 306 bits, record B 393, each in a line of 512) and 15.20 occupied.segment tree32.0 bits a valueseparate arrays12.1 bits a valueinterleaved records10.4 used, 15.2 heldbits a value: coloured, content; grey, padding to whole lines32,768 valuesshared table excluded
Fig. 6 Bits a value of each structure at 32,768 values, content in colour and padding to whole lines in grey, with the shared table and the caller’s values left out of all three.

The size result runs in two directions. The interleaved records hold less content than the separate arrays — 10.4 bits a value against 12.0 — because a count of ones in each record replaces two directories outright: the rank directory and the select support’s offsets, which come to 1.5 bits a value here and are most of what select is not rank backwards found a constant-time select costs. And they occupy more, 15.2 bits a value against 12.1, because record A uses 306 bits of its line and record B 393 of its, and the 325 bits left over in every superblock are 4.8 bits a value of padding.

That is a rise of a quarter in what two bits a value, and what undoes them found was already five times the payload, and it is not obviously worth it. The padding could be spent — a larger superblock fills record A, a smaller one empties record B — but each choice moves a different part of the line count, and the superblock size is also what sets the sparse tables’ size. A layout tuned to fill its lines is a different structure from the one measured, and this one is the plainest grouping that fits each record in a line.

The tree, at 32 bits a value, is still more than twice either.

What the layout decides, and what it does not

The ten-array layout and the two-line layout are the same structure. They answer every range identically, they make the same reads in the same order, and a description of the structure in terms of operations cannot tell them apart. Measured in lines, one costs 30% less than the other on isolated queries, both lose to a logarithmic structure on clustered ones, and the cheaper one is a quarter larger.

That is the position the layout that is told nothing describes from the other side, where the van Emde Boas layout made a search tree cheap at every block size without knowing any of them. Here the layout knows the line size exactly and is tuned for the isolated query, and the access pattern that matters — the listing’s batch of nested queries — is not the one it was tuned for. A heap that keeps its levels in a line found the same shape of result with a different structure: blocking a heap’s levels cut the lines of a sift and made the heap worse at the one buffer size where its blocks were mostly empty.

A layout is a bet on an access pattern. The succinct structure’s description does not choose one, which leaves the choice to whoever builds it, and the right choice for an index serving isolated lookups is the wrong one for an index serving a document listing.

What the replay leaves out

The cache model has no prefetcher, so neither layout is credited for the sequential walk a select makes through consecutive records, and the scan is not credited for reading consecutive lines of the document array. A real processor would make the scan cheaper and the interleaved select’s walk slightly cheaper, and neither change would move the order of the results.

A population count inside a line is charged nothing beyond the line. It is one or two instructions on any processor built in the last fifteen years, but it is still an operation the separate arrays’ rank did not need for the part its directory covered.

Still open: what a structure keeps between listings

Every listing here is priced cold: each distinct line it touches is one miss, and a line it touches twice is fetched once. That is already the most generous price a single listing can have — no cache of any size could make one listing cheaper than its distinct lines — so the next question is not inside a listing but between them. An index answering document queries answers thousands of them, one pattern after another, and what matters is what each structure still has in cache when the next pattern arrives.

The measurement that follows runs a workload of listings for patterns drawn from the collection’s own vocabulary, through an LRU cache of a few hundred lines held across the whole workload, and charges each listing its misses. The prediction is that the separate arrays gain most: at 32,768 values their rank directory is 25 lines and their select samples 16, small enough to stay resident across listings, so a listing would pay only for its parentheses, its tables and its values — about half its cold price — while the tree keeps its top levels and the interleaved records, which mix what every listing needs with what only this one does, keep least. It could fail on the vocabulary: patterns drawn from real text land all over the suffix array, and a cache of a few hundred lines may hold nothing a scattered workload re-reads. The question is whether a structure’s resident part — the part every query shares — is what decides its price in a running index, rather than anything a single query measures.

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.

Cache lineCrossoverDocument listingLocalityMemory layoutRange minimumRank directorySegment treeSelectSpace overheadSuccinctTrade off