The other axis

A select sample spaced by its chunk

A two-level directory over a chunked bit vector gives a select a sample every t values naming a rank block, then steps through rank samples every s bits until the count passes its target. With t fixed across chunks a select stepped through two or more rank samples, because a sample's values span more blocks in a sparse chunk than in a dense one. Spacing each chunk's select samples so that t values span about one rank block was predicted to bring the steps to about one, to reach under 70 bits read a select for under 3.65 bits a value on exact copies, and to widen the band where two levels pay to about 120 bits read. The steps fall to between 0.94 and 1.33 on every set. The other two predictions fail, and in the same direction: the extra samples the bitmap chunks now carry, and the partition the path cuts to avoid them, cost 0.11 bits a value on exact copies, and the band widens downward instead — under 60 bits read, where no fixed spacing reached, the scattered marks pay 7.44 bits a value against the single kinds' 7.86.

The run starts of an r-index’s Burrows–Wheeler transform are a sorted set of a few hundred to a few thousand values, and cuts that follow the gathering stored them as a partition into chunks — plain bitmaps, Elias–Fano lists or runs — cut where a shortest path over every possible cut says the bits are least. A select, the request for the jj-th value, finds its chunk through an upper level and then has to find the value inside it, and every essay since has been about the second half. A select that starts near its answer put a select sample every tt values inside each chunk. A sample that has to be found put a rank sample every ss bits instead, which bounds the scan but has to be searched for.

A second level that pays in one band used both. A select sample every tt values holds not a bit position but the number of the rank block that contains its value. A select reads that number, steps forward through the rank samples from there until one counts past its target, and scans at most ss bits inside the block it has reached. The prediction was one or two steps, and the measurement was more: on exact copies with tt and ss both 64, a select stepped through 2.21 rank samples, and with a sample every 256 values, 6.25. The directory reached 62 bits read a select for 3.76 bits a value, a point neither single kind reached, but under 3.6 bits a value it read 84, and across the eight sets the second level saved more than a twentieth of a bit a value only between about 65 and 100 bits read.

The steps had a cause that the counts made plain. Select samples are spaced in values and rank blocks in bits, so how many blocks lie between a sample and the value after it depends on how many bits a value takes in that chunk. A dense bitmap, two or three bits a value, puts 64 values inside about two blocks of 64 bits; a sparse Elias–Fano chunk of six or seven bits a value puts them across six or seven. One spacing for every chunk is right for at most one density.

A spacing for each chunk

The repair proposed was to space each chunk’s select samples by its own density. A chunk whose scanned vector is LL bits long for kk values carries d=L/kd = L/k bits a value, and t≈s/dt \approx s/d values span about one rank block. Here each chunk takes the power of two nearest s/ds/d, clamped between 8 and 512, so that tt values span between about 0.7 and 1.4 blocks; the spacing is a function of the chunk’s size, span and form, which the path already knows, and so it costs nothing to record beyond the samples it implies. Everything else is unchanged: the same sets, the same three widths of rank block, the same shortest path choosing the chunks with both levels’ bits charged, and the same model of reads — the upper level and chunk entry, the select sample, every rank sample stepped, and the bits scanned in the block.

The prediction had three parts. The steps would fall to about one a select on every set. On exact copies the directory would read under 70 bits a select for under 3.65 bits a value. And the band where two levels pay would widen from about 100 bits read to about 120. The risk named was the samples’ own bits: a chunk dense enough to need a sample every eight values carries a great many of them, each several bits wide, and that might cost more than the steps it saved.

The steps, on every set

With rank samples every 64 bits, a select steps through 0.94 to 1.33 rank samples on the eight sets when each chunk spaces its select samples to span about one block, against 1.15 to 2.21 with a sample every 64 values and up to 6.25 with one every 256Mean rank samples read a select under the two-level directory with rank samples every 64 bits, for select samples every 64 values, every 256 values, and every t values chosen per chunk as the power of two nearest s over the chunk's bits a value (8 to 512): the scattered marks: 1.57, 3.76, 1.11; exact copies: 2.21, 6.25, 1.33; divergence 0.0002: 2.19, 4.71, 1.28; divergence 0.0005: 1.62, 2.98, 1.08; divergence 0.001: 1.58, 1.99, 1.16; divergence 0.002: 1.15, 1.20, 0.99; divergence 0.005: 1.30, 2.30, 0.94; divergence 0.01: 1.62, 3.08, 1.12.the scattered marks1.11 per chunkexact copies1.33 per chunkdivergence 0.00021.28 per chunkdivergence 0.00051.08 per chunkdivergence 0.0011.16 per chunkdivergence 0.0020.99 per chunkdivergence 0.0050.94 per chunkdivergence 0.011.12 per chunkonebars, top to bottom: every 256 values · every 64 · spaced per chunkrank samples every 64 bitsrank samples stepped a select
Fig. 1 Rank samples stepped a select, with rank samples every 64 bits, on all eight sets: select samples every 256 values (top bar), every 64 values (middle) and spaced per chunk (bottom). Exact copies: 6.25, 2.21 and 1.33. Scattered marks: 3.76, 1.57 and 1.11. Divergence 0.01: 3.08, 1.62 and 1.12. Per chunk, every set lies between 0.94 and 1.33.

With rank samples every 64 bits, a select steps through between 0.94 and 1.33 rank samples on the eight sets once each chunk spaces its select samples to span about one block, against 1.15 to 2.21 with a sample every 64 values and up to 6.25 with one every 256. The first part of the prediction holds, and it holds everywhere: on the scattered marks, on exact copies, and at every divergence between. The variation that a fixed spacing produced across sets — a factor of nearly two at t=64t = 64 and of five at t=256t = 256 — is gone, because the variation was the sets’ densities and each chunk’s spacing now absorbs its own.

One is not quite the floor it sounds like. A select reads at least the rank sample of the block it lands in, and it reads a second when its target lies past the block its sample named. With tt values spanning about one block, the sample’s value sits somewhere inside its block and the values after it cross into the next block a good part of the time; that is the third of a step above one that exact copies show. The sets that come in just under one owe it to the last block of each chunk, which has no rank sample after it to read.

What the chunks chose

The spacings the chunks chose: on exact copies with rank samples every 64 bits, 54% of values take a select sample every 16 and 46% of values take a select sample every 32; no chunk on any of the three sets needed the densest spacing of 8, and the spacing doubles as s doublesFor the scattered marks, exact copies, divergence 0.01 and rank samples every 64, 128, 256 bits, the share of values (outside run chunks) whose chunk chose each select spacing t: the scattered marks s 64: t 32 2,049; the scattered marks s 128: t 64 2,049; the scattered marks s 256: t 128 2,049; exact copies s 64: t 16 361, t 32 305; exact copies s 128: t 32 417, t 64 249; exact copies s 256: t 64 423, t 128 243; divergence 0.01 s 64: t 16 142, t 32 3,989, t 64 13; divergence 0.01 s 128: t 32 123, t 64 4,021; divergence 0.01 s 256: t 64 167, t 128 3,977.the scattered marks, s = 64t 32the scattered marks, s = 128t 64the scattered marks, s = 256t 128exact copies, s = 64t 16t 32exact copies, s = 128t 32t 64exact copies, s = 256t 64t 128divergence 0.01, s = 64t 32divergence 0.01, s = 128t 64divergence 0.01, s = 256t 128share of values, by the select spacing their chunk chosespacing = power of two nearest s ÷ bits a valuevalues covered
Fig. 2 The share of values (outside run chunks) whose chunk chose each select spacing, on the scattered marks, exact copies and divergence 0.01, at rank samples every 64, 128 and 256 bits. Exact copies at 64 bits: 54% every 16 values, 46% every 32. Scattered marks: every 32, 64 and 128 values at the three widths. No chunk chose 8.

On exact copies with rank samples every 64 bits, 54% of values take a select sample every 16 and 46% one every 32; no chunk on any of the three sets needed the densest spacing of 8, and the spacing doubles as ss doubles. The scattered marks are twelve Elias–Fano chunks, and an Elias–Fano chunk’s scanned part is its high bits, which take about two bits a value however wide the gaps; every chunk chose the same spacing, 32 values at s=64s = 64, which is what 64/264/2 gives. Exact copies split between two, though not by kind. Apart from a two-value lead-in, every chunk the path chose there is a plain bitmap, at 2.8 to 3.5 bits a value; the two at 2.8 round to 32 and the three above 3.1 round to 16. A rounding boundary, not a difference in kind, runs through the middle of the set. At divergence 0.01 almost every value is in chunks of about two bits a value, with a few in sparser bitmaps.

The risk the proposal named, the chunk that needs a sample every eight values, does not occur, and the reason is in the forms. A spacing of 8 at s=64s = 64 is what a chunk needs when its scanned vector takes about eight bits a value. A bitmap that sparse is never chosen, since an Elias–Fano list codes the same values in fewer bits, and an Elias–Fano chunk scans only its high part, about two bits a value. So the scanned densities the path can produce are squeezed into a narrow range, roughly two to four bits a value, and the spacings into two powers of two. The risk arrives in a smaller form, a spacing of 16 rather than 8, and the next plate is where it is paid.

The spacings themselves say something the fixed directories hid. At t=64t = 64 every chunk on exact copies was spaced two to four times too widely for one-block steps. The directory that won at 62 bits read was a compromise between its densest and sparsest chunks, and the proposal’s diagnosis — that one spacing is right for at most one density — is visible in the split: no single power of two is chosen by more than about half of the values on exact copies.

Where the bits went

On exact copies with rank samples every 64 bits, spacing the select samples by chunk cuts the rank samples a select reads from 17.12 bits to 9.70 and the total from 62.01 to 54.82; the price is in the directory, 3.875 bits a value against 3.763On exact copies, rank samples every 64 bits: mean bits a select reads split into the upper level and chunk entry, the select sample, the rank samples stepped, and the scan in the block, with bits a value: every 64 values: total 62.01 (select 2.50, rank 17.12, block 31.85), 3.763 bits a value; every 128 values: total 69.49 (select 1.45, rank 25.66, block 31.85), 3.734 bits a value; every 256 values: total 87.56 (select 0.00, rank 49.81, block 31.90), 3.721 bits a value; spaced per chunk: total 54.82 (select 2.49, rank 9.70, block 31.45), 3.875 bits a value.every 64 values62.01 read · 3.763 b/vevery 128 values69.49 read · 3.734 b/vevery 256 values87.56 read · 3.721 b/vspaced per chunk54.82 read · 3.875 b/vupper and entryselect samplerank samplesblock scanexact copiesbits read a select
Fig. 3 On exact copies, rank samples every 64 bits: the bits a select reads split into the upper level and chunk entry, the select sample, the rank samples stepped and the scan in the block, for select samples every 64, 128 and 256 values and spaced per chunk. Totals 62.01, 69.49, 87.56 and 54.82 bits; bits a value 3.763, 3.734, 3.721 and 3.875.

On exact copies with rank samples every 64 bits, spacing the select samples by chunk cuts the rank samples a select reads from 17.12 bits to 9.70 and the total from 62.01 to 54.82; the price is in the directory, 3.875 bits a value against 3.763. The saving is exactly where the proposal put it. The select sample costs a select the same 2.5 bits either way, since it is one block number of the same width; the scan inside the block is the same 31 or 32 bits, half a block on average; and the rank samples stepped, which were the part that grew with the spacing, fall by more than seven bits. A select now reads 55 bits where the best fixed directory read 62.

The bill is 0.112 bits a value, and it has two parts. The select samples themselves are the larger: a chunk spaced at 16 carries four times as many as the same chunk spaced at 64, each one a block number of four or five bits, so the directory’s bits rise from 0.366 a value to 0.431. The rest is the partition. Charged for denser samples, the shortest path cuts the set into six chunks where it cut five, and the chunks it chooses code their values slightly less tightly.

The fixed directory’s partition kept a 40-value Elias–Fano chunk between two bitmaps; the per-chunk partition has none, and its five bitmaps are smaller. That second part is the reason the result is not just the first part scaled. The path sees the samples’ bits and nothing about reads, so when a spacing makes a chunk dearer in bits the path answers in bits, by rearranging the chunks, and the rearrangement is chosen without any regard for what the select then reads. The plate’s reads fell because the steps fell; the bits rose partly because the samples grew and partly because the path, minimising only bits, paid to avoid them.

The frontier on exact copies

On exact copies the chunk-spaced directory reads 54.82 bits a select for 3.88 bits a value, a point no fixed spacing reaches and the single kinds cost 3.93; at s = 128 and 256 its points sit on or just above the fixed spacings'On exact copies: bits a value against mean bits a select reads. The single kinds' undominated points: 1065.49 reads 3.270, 1065.49 reads 3.270, 525.77 reads 3.300, 379.88 reads 3.306, 288.96 reads 3.330, 170.17 reads 3.390, 111.07 reads 3.450, 97.86 reads 3.511, 77.14 reads 3.718, 42.90 reads 3.929. Two-level at fixed spacings t 64/128/256 and s 64/128/256: t 64 s 64 62.01 3.763, t 64 s 128 84.14 3.560, t 64 s 256 143.35 3.443, t 128 s 64 69.49 3.734, t 128 s 128 87.59 3.533, t 128 s 256 150.47 3.420, t 256 s 64 87.56 3.721, t 256 s 128 97.03 3.517, t 256 s 256 153.91 3.402. Spaced per chunk: s 64 54.82 3.875, s 128 80.27 3.586, s 256 139.93 3.438.3.403.603.80678log₂ bits a select readsbits a values 64s 128s 256spaced per chunksingle kindsfixed spacingsexact copiesa model of reads, not a timing
Fig. 4 Exact copies: bits a value against bits read a select (log scale). The single kinds’ undominated points (line), the two-level directory at fixed spacings (small dots), and spaced per chunk at s = 64, 128 and 256 (large dots). Per chunk: 54.82 bits read for 3.875 bits a value, 80.27 for 3.586, 139.93 for 3.438. The nearest fixed points: 62.01 for 3.763, 84.14 for 3.560, 143.35 for 3.443.

On exact copies the chunk-spaced directory reads 54.82 bits a select for 3.88 bits a value, a point no fixed spacing reaches and the single kinds cost 3.93 to reach; at ss = 128 and 256 its points sit on or just above the fixed spacings’. The second part of the prediction fails. Under 3.65 bits a value the cheapest read the chunk-spaced directory makes is 80.27 bits, at s=128s = 128, and the fixed spacing beside it reads 84.14 for 3.560. Spacing by chunk has moved each point to the left — fewer bits read — and up, by more than the move to the left is worth at that end of the frontier.

The three points tell the same story at three widths. At s=256s = 256 the chunk-spaced point is almost exactly the best fixed one, 139.93 bits read against 143.35 at a hundredth of a bit less; at that width a select already steps through about one rank sample with t=64t = 64, so there is little left to save, and the spacings chosen, 64 and 128, are close to the fixed ones anyway. At s=128s = 128 the move is four bits read for three hundredths of a bit a value. At s=64s = 64 it is seven bits read for eleven hundredths, and it lands on a point nothing else reached.

The prediction expected the gain at the cheap end of the frontier in bits and found it at the cheap end in reads. The proposal’s reasoning was that the steps were waste — reads paid for nothing — so removing them would be free. They were not paid for nothing. A wide spacing is a decision to store fewer samples and read more rank samples instead, and on the dense chunks that decision was a reasonable price; reversing it everywhere buys reads at a fixed exchange rate, which is worth paying only where reads are what is short.

Where the second level pays now

The chunk's own spacing widens the band where two levels pay downwards, not upwards: within 60 or 70 bits read it gives 3 sets a two-level point no fixed spacing reached — the scattered marks at 60, exact copies at 60, divergence 0.0005 at 70 — and within 100 it changes nothing on any setFor each set and budget of bits read a select (60, 70, 100): bits a value saved over the better single kind by the best two-level directory once spacing per chunk is allowed, "new" where no fixed spacing reached the budget, and in brackets the further saving over the fixed spacings. the scattered marks: 60: single 7.857, fixed two-level none, with per-chunk 7.436; 70: single 7.857, fixed two-level 7.368, with per-chunk 7.368; 100: single 7.233, fixed two-level 7.193, with per-chunk 7.193. exact copies: 60: single 3.929, fixed two-level none, with per-chunk 3.875; 70: single 3.929, fixed two-level 3.734, with per-chunk 3.734; 100: single 3.511, fixed two-level 3.517, with per-chunk 3.517. divergence 0.0002: 60: single 4.735, fixed two-level 4.608, with per-chunk 4.608; 70: single 4.568, fixed two-level 4.586, with per-chunk 4.586; 100: single 4.380, fixed two-level 4.380, with per-chunk 4.380. divergence 0.0005: 60: single 5.721, fixed two-level none, with per-chunk none; 70: single 5.721, fixed two-level none, with per-chunk 5.581; 100: single 5.347, fixed two-level 5.363, with per-chunk 5.363. divergence 0.001: 60: single 6.360, fixed two-level none, with per-chunk none; 70: single 6.174, fixed two-level 6.174, with per-chunk 6.174; 100: single 6.039, fixed two-level 6.039, with per-chunk 6.039. divergence 0.002: 60: single 6.748, fixed two-level 6.748, with per-chunk 6.748; 70: single 6.675, fixed two-level 6.675, with per-chunk 6.675; 100: single 6.651, fixed two-level 6.651, with per-chunk 6.651. divergence 0.005: 60: single 7.158, fixed two-level none, with per-chunk none; 70: single 6.846, fixed two-level 6.857, with per-chunk 6.857; 100: single 6.738, fixed two-level 6.738, with per-chunk 6.738. divergence 0.01: 60: single 6.520, fixed two-level none, with per-chunk none; 70: single 6.520, fixed two-level none, with per-chunk none; 100: single 6.178, fixed two-level 6.050, with per-chunk 6.050.within 60 bits readwithin 70 bits readwithin 100 bits readbits a value saved over the better single kindthe scattered marks0.42 new0.490.04exact copies0.05 new0.20-0.01divergence 0.00020.13-0.020.00divergence 0.0005none0.14 new-0.02divergence 0.001none0.000.00divergence 0.0020.000.000.00divergence 0.005none-0.010.00divergence 0.01nonenone0.13highlighted: a point no fixed spacing reachedbits a value saved
Fig. 5 For each set, the bits a value the best two-level directory saves over the better single kind within 60, 70 and 100 bits read a select, once spacing per chunk is allowed; highlighted where no fixed spacing reached the budget. Scattered marks within 60: 0.42 new. Exact copies within 60: 0.05 new. Divergence 0.0005 within 70: 0.14 new. Within 100 bits read the per-chunk spacing changes nothing on any set.

The chunk’s own spacing widens the band where two levels pay downward, not upward: within 60 or 70 bits read it gives three sets a two-level point no fixed spacing reached — the scattered marks at 60, exact copies at 60, divergence 0.0005 at 70 — and within 100 it changes nothing on any set. The third part of the prediction fails in the direction the frontier already suggested. The band was about 65 to 100 bits read a select, and its upper edge has not moved, because above 100 bits read the single kinds’ finest rank samples and the fixed two-level spacings were already as cheap as either can be. Its lower edge has moved from about 65 to under 60.

The largest of the new points is on the scattered marks. With rank samples every 64 bits and select samples every 32 values the marks read under 60 bits a select for 7.44 bits a value, where the better single kind needs 7.86 to read that little: four tenths of a bit a value, the largest saving any version of this directory has made on any set. The marks’ chunks are all Elias–Fano lists scanned at about two bits a value, so one spacing is right for all of them, and the reason no fixed directory found it is only that 32 was not among the fixed spacings measured. The spacing rule found it because it computes the spacing rather than choosing from a list.

That is worth saying plainly, because it limits what the plate shows. On a set of one density, the per-chunk rule is a fixed spacing that happens to be the right one, and its gain over a list of 64, 128 and 256 is partly a gain over a coarse list. On exact copies, where the chunks differ, the rule’s gain is the rule’s own; and there it is five hundredths of a bit at 60 bits read.

Why the steps were a price, not a waste

The earlier essay read 2.21 steps as a defect, and it was one in the sense that the prediction of one or two was wrong. But the steps were the cheap half of a trade. A select sample costs a block number’s width once per tt values; a step costs a rank sample’s width once per select that takes it. On a bitmap chunk of three bits a value with s=64s = 64, a block number is four or five bits and a rank sample about ten, so spacing at 16 instead of 64 adds about 0.2 bits a value to the chunk and saves about one rank sample — ten bits — a select. Whether that is a good trade depends entirely on how a bit stored compares with a bit read, and the path, which counts only bits stored, will never make it on its own.

The count is not the time made the general point for this collection’s earliest measurements: a model’s units decide what its optimum looks like. Here the units are stored bits for the path and read bits for the frontier, and every directory in these essays has been a choice of where to stand between them. Where the sparse representation loses found the same arithmetic one level up, choosing between a bitmap and an Elias–Fano list by density: the right representation was a function of the bits a value, and so, it now turns out, is the right spacing of a directory over either.

What the locating apparatus becomes is the reminder of what all of this is for. The run starts are one part of an r-index’s locating apparatus, and a select into them happens once per match located. A directory that saves seven bits read a select on exact copies saves them on every match, which on a collection of copies is most of the work; one that costs a tenth of a bit a value costs it once, on a set of a few hundred values. At these sizes the tenth of a bit is a few dozen bytes. Sparse on average, dense where it gathers measured how far that ratio moves as the indexed text grows: the set grows with the number of runs, the selects with the number of matches.

What the measurement does not settle

One shape of chunk. Each chunk’s spacing is set from its mean density. A chunk whose values gather unevenly inside it — dense at one end, sparse at the other — would want two spacings, and a chunk-wide mean gives it one. The partition tends to cut such chunks apart, and that may be part of why the path took six chunks on exact copies where it took five.

Powers of two. The spacing is rounded to the nearest power of two, so tt values span between 0.7 and 1.4 blocks rather than one. Any spacing would do as well for storage; powers of two are only convenient for the arithmetic that finds a sample.

Reads, not time. Every number is bits read under a model that charges a scanned bit, a sample and a block number alike per bit. A position split in two is where a select was first priced that way, and a machine that reads words rather than bits would charge the scan, which is half of everything here, very differently from the samples.

Still open: a spacing priced by what it saves

The plates say the spacing rule is right about the steps and wrong about the trade: it chooses each chunk’s spacing to make the steps one, whatever that costs in bits, while the path chooses the chunks to make the bits least, whatever that costs in reads. Neither sees both. A path could. If each chunk’s directory is charged its stored bits plus its expected read bits times an exchange rate — the selects into the chunk times the bits each reads, times λ\lambda bits stored per bit read — then the path can choose for each chunk among spacings of s/ds/d, 2s/d2s/d and 4s/d4s/d the one that is cheapest at that rate, and sweeping λ\lambda traces a frontier the fixed and per-chunk spacings only sample.

The measurement that follows does that, with selects assumed uniform over the values, at λ\lambda from a thousandth to a tenth, on all eight sets. The prediction is that on exact copies the priced path reaches under 60 bits read for under 3.80 bits a value, and under 70 for under 3.70, filling the gap between the per-chunk point and the fixed spacing at t=64t = 64; and that on the scattered marks it changes nothing, since one density has one right spacing. It could fail on the assumption about selects: on an r-index the selects follow the matches, not the values, and a chunk that holds the run starts of a common pattern is asked far more often than its share. The question is whether a directory can be fitted to a rate of exchange between storage and reading, or whether the rate itself depends on which values are asked for — and the path, which sees only the set, cannot know.

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.

Compressed bit vectorElias fanoIndex sizeR-indexRank querySelectSparse set