Structures

The lines a constant-time query lands in

Counted in reads, the succinct range minimum answers a query in 16.9 against the segment tree's 41.7, and it moved the document listing's crossing from 32 occurrences a document to 10.8. Counted in the 64-byte lines those reads land in, it costs 17.9 lines against 19.8 at 32,768 values and more than the tree below that, because its reads go to ten different arrays and the tree's top levels share a line. The scan it competes with reads 170 document entries to a line, so priced in lines the chain overtakes it at about 7,950 occurrences with the tree and 12,140 with the succinct structure.

Where a crossing moved to replaced the document listing’s segment tree with the range minimum the method is published with — a parenthesis sequence of two bits a value, answered in a constant number of lookups — and measured what the swap was worth. The listing finds the documents holding a pattern by asking a range minimum over a chain of previous occurrences: twice the answer’s size in queries, plus one, whatever the number of occurrences. Its rival reads every occurrence. Priced in node visits and table reads, the chain had been paying about 25 reads a query to the tree and paid about 10 to the succinct structure, and the point where it overtook the scan moved from 257 occurrences to 86 — from 32 a document to 10.8, on eight documents.

That essay ended by naming the unit it had used and not defending it. A read is one access the structure chose to make. It is not what a machine charges for. The count is not the time is where this collection first separated the two: a processor fetches memory in lines of 64 bytes, a read that lands in a line already fetched costs almost nothing, and a read that lands in a new one costs what a trip to memory costs. Two structures that make the same number of reads can differ by a large factor in lines, in either direction. So the question here is the old crossing asked in the unit the machine bills: how many distinct lines does each structure’s query touch, and where does the listing cross the scan when both are priced that way?

A layout has to be stated before a line can be counted

A line count is a property of where things sit, so the first thing to fix is a layout, and the one used throughout is the plainest that is faithful to each structure’s own size accounting.

The segment tree is one array of 2n2n nodes in breadth-first order, each node ⌈log⁡2n⌉\lceil \log_2 n \rceil bits wide — the width the cost that is the size of the answer charged it. At 32,768 values that is fifteen bits, so a 512-bit line holds 34 nodes, and the root’s line holds the whole of the top five levels.

The succinct structure is the parts two bits a value, and what undoes them weighed it in, each packed at its own width and each starting on a line of its own: the parentheses, the select support’s samples and offsets, the rank directory, the in-block answer table the table that fits inside a block built, the superblock minima and their sparse table, the block minima and the per-superblock tables over them, and the array of values the caller holds. A read of entry ii of an array ww bits wide lands in line ⌊iw/512⌋\lfloor iw/512 \rfloor of that array; a read of a run of bits — a block of parentheses, the tail of a rank inside its block — lands in every line the run crosses.

Two choices in that list are charged as the published structure would be, not as the implementation stores them. The excess at a position is not stored in the published structure; it is 2 rank1(t)−t2\,\mathrm{rank}_1(t) - t, so every excess the query consults is charged here as the rank it is — a directory entry and the parenthesis bits up to tt. And a block’s shape is not a separate array either; it is the block’s own parentheses, so reading it is reading the payload.

Every replayed query is required to return the position the real structure returned, on every query of every measurement below. A replay that drifted from the walk it claims to describe would be counting lines for some other walk, and the check makes that a failure rather than a number.

Two and a half times apart in reads, one in lines

The first plate is one query, priced both ways, at four array sizes.

At 32,768 values a query costs the tree 41.7 counted reads and the succinct structure 16.9 — 2.47 times apart — but 19.8 lines against 17.9, 1.11 times; at 512 the tree touches fewer lines (6.9 against 11.9)200 random ranges at each size over a chain-shaped array. Reads are the acts the structures' own counters charge (a node visited; a table entry, select or rank). Lines are distinct 64-byte lines a query touches with each array packed at the width its size accounting charges and the succinct structure's excess reads charged as the ranks they are. 512: tree 23.3 reads, 6.88 lines; succinct 15.5 reads, 11.94 lines; 2,048: tree 29.8 reads, 11.24 lines; succinct 16.4 reads, 14.38 lines; 8,192: tree 35.8 reads, 15.79 lines; succinct 16.8 reads, 16.60 lines; 32,768: tree 41.7 reads, 19.84 lines; succinct 16.9 reads, 17.88 lines. Every replayed query returned the position the structure returned.010203040values in the array (log scale)a query: reads counted, and distinct lines5122,0488,19232,768tree, readstree, linessuccinct, linessuccinct, reads200 random ranges a sizereads against lines
Fig. 1 Each structure twice: the reads its own counter charges, and the distinct lines those reads land in. The tree’s two curves are far apart because its reads share lines; the succinct structure’s are close together because they do not.

In reads the picture is the one where a crossing moved to drew. The tree climbs from 23.3 to 41.7 reads across a sixty-fourfold growth in the array, which is its two logarithms — the descent to the range’s boundary and the descent to the minimum’s leaf. The succinct structure sits at 15.5 to 16.9, flat, which is what a constant number of lookups looks like on random ranges. At 32,768 values they are 2.47 times apart.

In lines the gap is almost gone. At 32,768 values the tree touches 19.8 lines a query and the succinct structure 17.9, a factor of 1.11. Below that the order reverses: at 8,192 the tree touches 15.8 to the succinct structure’s 16.6, at 2,048 it is 11.2 to 14.4, and at 512 it is 6.9 to 11.9. On every array smaller than about twenty thousand values the structure built to answer in constant time touches more lines than the one that answers in logarithmic time. It still grows less: the tree’s lines nearly triple across the sweep and the succinct structure’s rise by half. Somewhere past the largest size measured the constant must win, because a logarithm does not stop. But the size at which a reader would notice is not where the asymptotic reading puts it.

The reason is visible in the ratio of each structure’s two curves. The tree spends 41.7 reads to touch 19.8 lines — about two reads a line — because its reads are concentrated: the top of every descent is the same few nodes, those nodes sit together at the front of the array, and a descent’s last few steps move between siblings that share a line. The succinct structure spends 16.9 reads to touch 17.9 lines, which is more lines than reads, because several of its reads are runs of parenthesis bits that cross a line boundary, and because almost nothing it reads sits next to anything else it reads.

Where the seventeen lines are

Splitting the succinct structure’s lines by the array they fall in shows why no single improvement fixes it.

A succinct query at 32,768 values touches 17.9 lines spread over 10 arrays, none contributing more than 2.1: the reads that the counter calls one select, one rank or one table entry are each a line of their own, and the tree's 19.8 lines sit in one array whose top levels share themDistinct 64-byte lines a query touches in each array of the succinct range minimum, averaged over 200 random ranges of 32,768 values: select samples 1.84, select offsets 1.98, rank directory 1.91, parentheses 2.06, shared table 1.99, superblock minima 1.56, superblock table 1.42, block minima 2.12, block tables 2.00, values 1.00; total 17.88. The segment tree on the same queries: 19.84 lines in its one array. The structure's footprint is 1,896 lines and the tree's 2,048.select samples1.84select offsets1.98rank directory1.91parentheses2.06shared table1.99superblock minima1.56superblock table1.42block minima2.12block tables2.00values1.00segment tree on the same queries: 19.84 lines, all in one arraysuccinct: 17.88 lines a query, by array32,768 values, 200 rangeslines of 512 bits
Fig. 2 The lines a succinct query touches, by array, at 32,768 values. Ten arrays, each contributing one or two lines and none more than about two; the tree’s total sits below at one eighth of the scale it would need.

No array dominates. The select samples take 1.84 lines a query, the select offsets 1.98, the rank directory 1.91, the parentheses 2.06, the shared table 1.99, the superblock minima 1.56, the superblock table 1.42, the block minima 2.12, the block tables 2.00, and the value at the answer 1.00. Each of the structure’s two selects lands in the samples, the offsets and the parentheses; the ±1 query lands in the parentheses again, the table, two levels of minima and two levels of tables; the final rank lands in the directory and the parentheses. A query’s two endpoints are, on random ranges, far apart, so each of those arrays is visited twice in two different places.

That is the arithmetic of a structure assembled from parts, and it is not a defect of any one part. Every part is small — the whole structure occupies 936 lines at 32,768 values, 1,896 with the array of values it reads its answer from, against the tree’s 2,048 with its values in its leaves — and every part was designed to be small, not to be read alongside the others. The published description calls the query “constant time” and counts it in word operations, which is the right count for the claim being made. It says nothing about how many separate places in memory the constant number of operations are scattered over, and the answer is about as many as there are operations.

The tree, by contrast, is one array, and a descent through one array in breadth-first order spends its first five levels inside a single line. Its weakness is depth, and depth costs lines only at the bottom of a descent, where a node and its children stop sharing a line. The bucket that fits a line made the same observation about a hash table from the other side: a structure whose every operation is two reads still costs two lines only if each read is a line, and laying the reads out together is a separate decision from deciding how many there are.

A stream of queries, and what each structure keeps

The cold price — every line a miss — is the price of one isolated query. A structure queried continuously keeps what it re-reads, so the second measurement replays a stream of 2,000 random queries through a fully associative cache of lines with least-recently-used replacement, and counts misses.

Over a stream of 2,000 queries the succinct structure misses fewer lines with no cache at all (34.7 against 46.1), with 8 lines, and with the whole structure resident; with 32 to 512 lines cached the tree misses fewer (10.0 against 13.0 at 128), because what it re-reads is its own top levels2,000 random ranges of a 32,768-value array replayed through a fully associative LRU of K lines: misses a query. K = 0: tree 46.08, succinct 34.72; K = 8: tree 23.28, succinct 20.25; K = 32: tree 14.49, succinct 16.83; K = 128: tree 9.97, succinct 13.01; K = 512: tree 5.00, succinct 5.38; K = 2048: tree 1.00, succinct 0.85. Footprints: tree 2,048 lines, succinct 1,896. The succinct structure misses fewer at K = 0, 8, 2048.02040lines the cache holds (log scale; first point: none)misses a query, over a stream of queriesnone8321285122,048segment treesuccinct32,768 values, LRU of linesmisses a query
Fig. 3 Misses a query against the lines the cache holds, both structures, on 32,768 values. With no cache every read is a miss; with the whole structure resident nothing is. In between, the tree keeps its top levels and the succinct structure has nothing it re-reads as often.

With no cache at all, every read is a miss, and the plate’s first point is simply the reads: 46.1 for the tree, counting both children at each step of its descent, against 34.7 for the succinct structure. With eight lines the succinct structure still misses fewer, 20.3 against 23.3. From 32 lines the order turns: 14.5 for the tree against 16.8, then 10.0 against 13.0 at 128 lines, and 5.0 against 5.4 at 512. Only when the cache holds nearly all of both structures, at 2,048 lines, does the succinct structure miss less again, because it is slightly smaller.

What the tree keeps is easy to name. Every query passes through the root and most pass through the first ten levels, which occupy a few dozen lines; once those are resident, a query misses only on the lines of its two descents that are deep enough to be specific to it. The succinct structure has no equivalent. Its directories are shared by every query but are each indexed by position, so a query near one end of the array and a query near the other read different lines of every one of them. The only part read by every query is the shared table of in-block answers, and at the block length of eight used here that table is 162 lines — more than a small cache holds, and read at a different entry by almost every query.

This is the regime the layout that is told nothing is about: a cache of some size nobody chose, holding whatever the access pattern makes popular. A structure whose popular part is its top is well served by any cache; a structure that has no popular part is served only by a cache that holds all of it.

The listing, priced in lines

The crossing is where all of this was supposed to matter, so the third measurement is where a crossing moved to’s own sweep: eight documents of 2,048 characters, a six-character pattern planted at rising density in three of them, the answer fixed at eight documents while the occurrences rise from 32 to 792.

The scan’s price is the lines of the document array it reads. Those are contiguous rows of the suffix array, one document number each, and with eight documents a document number is three bits — so a line holds 170 of them. It also marks each document in a bitmap of eight bits, one line in all. The chain’s price is every line its range structure reads across the whole listing, plus the chain value at each answer and the document it reports.

Priced in 64-byte lines, neither chain overtakes the scan anywhere on the sweep — at 792 occurrences the scan reads 7 lines against 24 and 31: the scan reads 171 document entries to a lineDocument listing on 8 documents of 2,048 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. 32 occurrences: scan 2, tree 13 (204 counted visits), succinct 12 (49); 41 occurrences: scan 2, tree 13 (238 counted visits), succinct 13 (76); 77 occurrences: scan 2, tree 16 (238 counted visits), succinct 17 (86); 221 occurrences: scan 3, tree 21 (247 counted visits), succinct 22 (92); 408 occurrences: scan 4, tree 22 (300 counted visits), succinct 26 (118); 792 occurrences: scan 7, tree 24 (258 counted visits), succinct 31 (98). Crossing in lines: tree not reached, succinct not reached.100251020occurrences of the pattern (log scale)distinct lines a listing touches (log scale)chain, succinctchain, segment treescan8 documents of 2,048lines of 512 bits
Fig. 4 The same eight-document sweep on which the chain crossed the scan at 257 occurrences with the tree and 86 with the succinct structure, priced in lines. Neither chain reaches the scan anywhere on it.

Neither crossing survives. At 32 occurrences the scan reads 2 lines, the tree’s chain 13 and the succinct chain 12; at 792 occurrences the scan reads 7, the tree’s chain 24 and the succinct one 31. The scan is cheaper at every point of the sweep, by a factor of three or more, and the plate has no crossing on it to mark.

Two things changed at once, and they are worth separating. The scan got cheaper by a factor that grows with the occurrences — at 792 of them, 792 reads became 7 lines — because 170 consecutive reads are one line. The chain did not get cheaper by anything like that. Its eleven or twelve queries are nested inside one range and share many of their lines, but each still reaches arrays and depths the others do not. And the chain’s price, flat in reads, is not flat in lines. It rises from 13 to 24 with the tree and from 12 to 31 with the succinct structure, because a wider range sends the listing’s queries to positions further apart, and positions further apart share fewer lines. The output-sensitive claim — the cost follows the answer, not the range — holds in queries and in reads. In lines it holds only approximately, and the succinct structure, with ten arrays to land in, pays for the spread more than the tree does.

How far the crossing moved

A sweep that never reaches the crossing cannot say where it is, so the same planting was repeated on documents of 32,768 characters, where a pattern can be planted four thousand times in each of three documents.

Priced in 64-byte lines, the chain overtakes the scan at 7,952 occurrences with the segment tree and 12,143 with the succinct structure: 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), succinct 26 (110); 579 occurrences: scan 6, tree 28 (429 counted visits), succinct 29 (109); 1,140 occurrences: scan 9, tree 33 (471 counted visits), succinct 41 (136); 3,429 occurrences: scan 22, tree 43 (614 counted visits), succinct 57 (177); 6,419 occurrences: scan 39, tree 46 (650 counted visits), succinct 67 (180); 12,552 occurrences: scan 75, tree 54 (609 counted visits), succinct 73 (163). Crossing in lines: tree 7,952, succinct 12,143.10³10⁴5102050occurrences of the pattern (log scale)distinct lines a listing touches (log scale)scanchain, succinctchain, segment treetree crosses at 7,952succinct crosses at 12,1438 documents of 32,768lines of 512 bits
Fig. 5 The listing on eight documents of 32,768 characters, the occurrences rising from 441 to 12,552. The scan’s line count climbs at three bits an occurrence; the chains climb slowly; the crossings are interpolated between the two points that bracket each.

The tree’s chain crosses the scan at 7,952 occurrences and the succinct chain at 12,143, interpolated between measured points at 6,419 and 12,552 occurrences. Per document that is 994 and 1,518 — against 32 and 10.8 in reads.

So in lines the swap of range structures moved the crossing the other way. In reads, the succinct structure brought the crossing down by a factor of three; in lines, it pushes it up by a factor of one and a half, because the chain’s line price is set by how scattered its reads are rather than by how many there are, and the succinct structure is the more scattered of the two. The essay that measured the reads was right about what it measured. The unit it measured in was the one that made the structure look best.

The occurrence counts at which either chain wins are not exotic, and the crossing that never arrives listed where they live: a version history, a long book, a log of machine-written records. What changes is the bar. A common word in a book does occur a thousand times. A phrase in a collection of short documents does not, and on a collection whose documents are under a few thousand characters the chain, priced in lines, never pays for itself on any query at all.

The bar, by collection

The crossing per document depends on two things the eight-document collection fixed: how many documents there are, which sets how many entries the scan gets to a line, and how much the chain’s queries share, which depends on how many of them there are and how close together.

Occurrences each answered document must carry before the chain beats the scan: in reads 8.2, 7.8, 5.8 with the succinct structure at 8, 32, 128 documents; in lines 398, 109, 50 — and in lines the segment tree needs fewer (284, 73, 27)Collections of 8, 32, 128 documents of 512 characters with the pattern planted 8 times in a third of them. For each, the chain's price per answered document — counted visits, then distinct lines converted at the scan's rate of 512/⌈log₂ d⌉ entries a line — which is the occurrences a document must carry for the chain to win. 8 documents (answer 6, 3-bit entries): reads tree 26.3, succinct 8.2; lines tree 284.4, succinct 398.2; lines a query tree 1.25, succinct 1.75; 32 documents (answer 31, 5-bit entries): reads tree 25.1, succinct 7.8; lines tree 72.7, succinct 109.0; lines a query tree 0.63, succinct 0.94; 128 documents (answer 121, 7-bit entries): reads tree 26.5, succinct 5.8; lines tree 27.2, succinct 50.2; lines a query tree 0.33, succinct 0.61.8 documents3-bit entries26.3 tree, reads8.2 succinct, reads284 tree, lines398 succinct, lines32 documents5-bit entries25.1 tree, reads7.8 succinct, reads72.7 tree, lines109 succinct, lines128 documents7-bit entries26.5 tree, reads5.8 succinct, reads27.2 tree, lines50.2 succinct, linesoccurrences an answered document needs (log scale)documents of 512 charactersreads against lines
Fig. 6 For collections of 8, 32 and 128 documents, the occurrences each answered document must carry before the chain beats the scan: priced in reads, and priced in lines at the collection’s own chain price. A logarithmic axis, because the two prices differ by up to fifty times.

In reads the bar barely moves: the succinct chain needs 8.2, 7.8 and 5.8 occurrences per answered document at 8, 32 and 128 documents, and the tree’s chain about 26 at all three. In lines it moves a great deal: 398, 109 and 50 for the succinct chain, and 284, 73 and 27 for the tree’s. Two effects push it down as the number of documents grows. A document number gets wider — three bits at eight documents, seven at 128 — so the scan’s line holds fewer of them. And the answer gets larger, so the chain’s queries become numerous and close together, and they start sharing lines: at 128 documents the tree’s chain spends a third of a line a query and the succinct chain six tenths.

The order of the two structures does not change. In lines the tree’s chain needs fewer occurrences than the succinct chain on every collection measured, by between 1.4 and 1.8 times.

What constant time was measured against

None of this contradicts what the succinct structure claims. It answers in a constant number of word operations, and it does: 15.5 to 16.9 across the sizes measured, flat. It occupies close to two bits a value, plus the overhead two bits a value, and what undoes them priced. Both claims are about the structure, and both are true.

What the measurement shows is the position the constant the notation drops describes, one level down. A constant in operations becomes a different constant in lines, and the conversion rate is a property of layout that the operation count does not see. For the tree the rate is about two operations a line, and for the succinct structure about one. At the sizes a document listing runs at, that difference is larger than the logarithm the succinct structure was built to remove.

What O-notation does not say is the general warning, and this is the case where its warning cuts against the newer structure: the asymptotic class is better, the constant in operations is better, and the cost a machine charges is worse until the arrays are larger than any collection measured here.

What the replay leaves out

The cache model is fully associative with least-recently-used replacement, and real caches are neither. More important for this comparison, real processors prefetch: a scan through consecutive lines is detected and fetched ahead, so its lines arrive before they are asked for. The scan in this essay is charged a full miss for every line it reads, and on a machine with a prefetcher it would cost less than that. Every correction the model leaves out favours the scan, which means the crossings above are low estimates.

The layout is also the plainest one. Each array of the succinct structure starts on a fresh line and is read where it lies; a layout that put each block’s minima, its rank directory entry and its table row in one line would cut the regions plate’s ten arrays to a few. That layout is the published structure’s to choose, and none of its descriptions choose it.

Still open: a layout that puts a block’s parts in one line

The regions plate is the case for the next measurement. Every one of the succinct structure’s lines a query lands in is in a separate array, but most of them are indexed by the same thing — a block, or the superblock above it. The rank directory entry, the block’s minimum and its offset into the superblock’s table are all read for the same block, and the select sample that lands in a block points into the same parentheses.

The measurement that follows interleaves them: one record per block holding its parentheses, its rank count, its minimum and its in-superblock table row, padded to a line, with the superblock’s parts in a record of their own. The prediction is that a query falls from about 17.9 lines to about 8 at 32,768 values — the two endpoints’ blocks, their superblocks, the shared table, the middle superblock range’s two table rows and the value — and that the listing’s crossing comes back under the tree’s, near 5,000 occurrences on the long collection. It could fail on size: a record padded to a line costs bits the separate arrays did not, and at seven parentheses a block the padding could double the structure. The question is whether a constant-time structure can be made to touch a constant number of lines without giving up the size it was built for.

What this makes readable

Essays that name this one as a prerequisite.

Named alongside this one

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

What links here

Every essay whose body links to this one.

The objects this essay names

Each one links to every other essay that touches it.

Cache lineConstant factorCrossoverDocument listingLocalityMemory layoutOutput-sensitiveRange minimumRank directorySegment treeSelectSuccinct