The other axis

A second level that pays in one band

A select on a chunked bit vector can be helped two ways: a select sample every t values says where to start but not how far to scan, and a rank sample every s bits bounds the scan but must be searched for. A directory with both — select samples that name a rank block, rank samples that bound the scan — was predicted to reach under 70 bits read a select for under 3.6 bits a value on an r-index's run starts over exact copies, a point neither kind reaches. It reaches 62 bits read for 3.76 bits a value, which neither kind reaches, and costs 3.73 within 70 bits where the better single kind costs 3.93; but under 3.6 bits a value it reads 84. The select sample names a block two rank samples away, not one. Across eight sets the second level saves more than a twentieth of a bit a value on three, and only between about 65 and 100 bits read a select: outside that band either single kind is as good.

An r-index locates its matches through the positions where the runs of its Burrows–Wheeler transform start, a sorted set of a few hundred to a few thousand values over a text of sixty-five thousand characters, and cuts that follow the gathering stored that set as a partition: chunks of consecutive values, each coded as a plain bitmap, an Elias–Fano list or a run, cut where a shortest path over every possible cut says the total bits are least. On copies of one text the run starts gather in a few dense stretches, and the cheapest partition put most of them in one large bitmap chunk. Every select into that chunk — find the jj-th value — scanned the bitmap from its start: 1,065 bits read a select, against 33 for a single Elias–Fano list over the whole set.

The essays since have given the chunks directories. A select that starts near its answer put a select sample every tt values inside each chunk, the bit position of every tt-th value, so a select jumps to the sample before its value and scans from there: every 64 values, 111 bits read a select for 3.45 bits a value. A sample that has to be found tried rank samples instead, the count of values before every ss-th bit, which bound the scan to ss bits but have to be searched for, and found the two kinds drew one frontier with rank samples coming in finer steps.

Its closing section saw that each kind removes one of a select’s two kinds of ignorance. A select sample knows which stretch to start in and not how far to scan; a rank sample bounds the scan and does not know where it is. A two-level directory gives a select both: a select sample every tt values that holds not a bit position but the number of the rank block containing its value, and rank samples every ss bits. A select reads its select sample by arithmetic, which names a block near its target; steps forward through the rank samples until the count passes its rank, usually one or two of them, it said; and scans at most ss bits. The prediction was under 70 bits read a select for under 3.6 bits a value on exact copies, and under 70 for about 6.2 at a divergence of 0.01, points neither kind reaches. The named risk was the second level’s bits in chunks only moderately long, where the path might cut those chunks instead and leave the frontier where it was.

Both levels, charged in the path

The sets, partition and charges are the earlier essays’. A select is charged the bits it reads: the upper level that finds its chunk, then inside the chunk the directory and the scan. Under the two-level directory a chunk whose scanned vector — its bitmap, or its Elias–Fano high part — is LL bits long holds ⌊(L−1)/s⌋\lfloor (L-1)/s \rfloor rank samples, each as wide as the chunk’s count of values needs, and a select sample every tt values, each wide enough to name a rank block. A select for the jj-th value reads the select sample before it, which names the block b0b_0 holding that sampled value; reads the rank samples after b0b_0 one by one until one counts past jj; and scans from the start of the block it has found to its value’s bit. Those bits are charged in the shortest path, so the partition is chosen with both levels’ price in view.

The directory was run at every combination of tt = 64, 128, 256 values and ss = 64, 128, 256 bits, on eight sets: exact copies of one text, copies at six divergences from 0.0002 to 0.01, and scattered marks with no gathering at all.

A point neither kind reaches, and not the one predicted

On exact copies the two-level directory reaches 62 bits read a select for 3.76 bits a value (a select sample every 64 values naming a rank block, rank samples every 64 bits), and within a budget of 70 bits read it costs 3.734 bits a value where the better single kind costs 3.929; outside about 65 to 100 bits read it lands on the single kinds' frontierOn exact copies: bits a value against the mean bits a select reads, for the cheapest uncapped partition with select samples alone (every 256, 64, 16 values), rank samples alone (every 1024, 512, 256, 128, 64 bits), and both levels at t = 64, 128, 256 and s = 64, 128, 256. Two levels: t 64 s 64: 3.763 for 62 (2.21 rank samples stepped), t 64 s 128: 3.560 for 84 (1.70 rank samples stepped), t 64 s 256: 3.443 for 143 (1.24 rank samples stepped), t 128 s 64: 3.734 for 69 (3.31 rank samples stepped), t 128 s 128: 3.533 for 88 (2.18 rank samples stepped), t 128 s 256: 3.420 for 150 (1.70 rank samples stepped), t 256 s 64: 3.721 for 88 (6.25 rank samples stepped), t 256 s 128: 3.517 for 97 (3.45 rank samples stepped), t 256 s 256: 3.402 for 154 (2.12 rank samples stepped). Select samples: 3.270 for 1,065, 3.306 for 380, 3.450 for 111, 3.929 for 43. Rank samples: 3.270 for 1,065, 3.300 for 526, 3.330 for 289, 3.390 for 170, 3.511 for 98, 3.718 for 77.3.203.403.603.8056789log₂ bits a select readsbits a valuet 64, s 64two levelsselect samples alonerank samples aloneexact copiesa model of reads, not a timing
Fig. 1 Exact copies: bits a value against log₂ bits read a select, for select samples alone, rank samples alone, and the two-level directory at nine settings (faint points are beaten on both counts). Two levels at t 64, s 64: 3.763 bits a value for 62 bits read; t 64, s 128: 3.560 for 84; t 128, s 128: 3.533 for 88. Select samples every 16 values: 3.929 for 43; every 64, 3.450 for 111. Rank samples every 64 bits: 3.718 for 77; every 128, 3.511 for 98.

On exact copies the two-level directory reaches 62 bits read a select for 3.76 bits a value, with a select sample every 64 values naming a rank block and rank samples every 64 bits, and within a budget of 70 bits read it costs 3.734 bits a value where the better single kind costs 3.929; outside about 65 to 100 bits read it lands on the single kinds’ frontier. Under 70 bits read, only select samples every sixteen values compete, at 3.93 bits a value, and rank samples cannot get there at all: their finest setting, every 64 bits, reads 77. The two-level directory fills that gap, a fifth of a bit a value cheaper than the only thing that reaches it.

But the prediction asked for both halves at once — under 70 bits read and under 3.6 bits a value — and no setting gives them. Under 3.6 bits a value the two-level directory’s cheapest read is 84 bits, at t=64t = 64 and s=128s = 128, where rank samples every 128 bits read 98 for 3.51. From about a hundred bits read upward, rank samples alone are as good: within 100 bits the two-level directory costs 3.517 and rank samples 3.511. The second level found a point, 62 bits for 3.76, that nothing else offered; it did not find the point it was designed for.

Two steps, not one

With a select sample every 64 values and rank samples every 64 bits, a select on exact copies reads 2.50 bits of select sample, 17.12 of rank samples — 2.21 of them stepped, not the one or two predicted — and 31.85 in its block; across the sets the rank samples stepped are 1.15 to 2.21 a selectThe mean bits a select reads under the two-level directory at t = 64, s = 64, split into the upper level and chunk entry, the select sample, the rank samples stepped through, and the scan inside the target block, per set: the scattered marks: total 60.91, select 1.82, rank 12.24 (1.57 samples), block 32.08; exact copies: total 62.01, select 2.50, rank 17.12 (2.21 samples), block 31.85; divergence 0.0002: total 57.58, select 1.92, rank 16.51 (2.19 samples), block 30.07; divergence 0.0005: total 70.45, select 0.85, rank 9.29 (1.62 samples), block 28.54; divergence 0.001: total 66.24, select 0.22, rank 5.69 (1.58 samples), block 26.91; divergence 0.002: total 58.80, select 0.03, rank 3.45 (1.15 samples), block 24.84; divergence 0.005: total 68.73, select 0.32, rank 6.90 (1.30 samples), block 28.91; divergence 0.01: total 73.59, select 1.24, rank 10.67 (1.62 samples), block 30.17.the scattered marks61 · 1.57 stepsexact copies62 · 2.21 stepsdivergence 0.000258 · 2.19 stepsdivergence 0.000570 · 1.62 stepsdivergence 0.00166 · 1.58 stepsdivergence 0.00259 · 1.15 stepsdivergence 0.00569 · 1.30 stepsdivergence 0.0174 · 1.62 stepsupper level · select sample · rank samples · block scant 64, s 64bits read a select
Fig. 2 With a select sample every 64 values and rank samples every 64 bits, the bits a select reads on each set, split into the upper level and chunk entry, the select sample, the rank samples stepped through, and the scan inside the target block. Exact copies: 62 in all, 2.5 of select sample, 17.1 of rank samples (2.21 samples), 31.9 in the block. Across the sets the rank samples stepped run from 1.15 to 2.21 a select.

With a select sample every 64 values and rank samples every 64 bits, a select on exact copies reads 2.50 bits of select sample, 17.12 of rank samples — 2.21 of them stepped, not the one or two predicted — and 31.85 in its block; across the sets the rank samples stepped are 1.15 to 2.21 a select. The block scan is what the rank samples promise: at most 64 bits, 32 on average. The select sample is nearly free, two or three bits read once. The stepping is not: a select whose value is among its chunk’s first 64 has no select sample to read and starts at block nought, and every other select starts at the block of a value up to 63 places before its own.

Between that block and the target’s lie as many rank blocks as those values span, and the 2.21 steps a select measured on exact copies say that 64 values of these chunks span two or three blocks of 64 bits. The prediction’s “one or two” assumed a select sample would land in or next to the target’s block, which needs tt values to span about one block of ss bits, and on dense chunks they span several.

Spacing against steps

On exact copies a select steps through 2.21 rank samples with a select sample every 64 values and rank samples every 64 bits, and 6.25 with select samples every 256 values: the farther apart the select samples, the more rank blocks lie between a sample's value and the target, so the steps grow with t and shrink as s widens the blocksOn exact copies, rank samples read a select under the two-level directory, against the select samples' spacing t, for rank samples every 64, 128, 256 bits: s 64: t 64 2.21, t 128 3.31, t 256 6.25; s 128: t 64 1.70, t 128 2.18, t 256 3.45; s 256: t 64 1.24, t 128 1.70, t 256 2.12.0246values between select samplesrank samples stepped a select64128256rank samples every 64 bitsrank samples every 128 bitsrank samples every 256 bitsexact copiesrank samples stepped
Fig. 3 Exact copies: rank samples stepped a select against the select samples’ spacing t, for rank samples every 64, 128 and 256 bits. At s 64: 2.21, 3.31 and 6.25 at t 64, 128 and 256. At s 128: 1.70, 2.18, 3.45. At s 256: 1.24, 1.70, 2.12.

On exact copies a select steps through 2.21 rank samples with a select sample every 64 values and rank samples every 64 bits, and 6.25 with select samples every 256 values: the farther apart the select samples, the more rank blocks lie between a sample’s value and the target, so the steps grow with tt and shrink as ss widens the blocks. A step reads one rank sample, about eight bits wide on these chunks, so at t=256t = 256 and s=64s = 64 the stepping costs 50 bits a select, more than the block scan it was meant to save. The directory’s two parameters are not independent: the stepping is set by how many rank blocks tt values span, which is tt times the chunk’s bits per value over ss, and the chunks’ density decides it.

That ratio is what the proposal fixed without measuring. A two-level directory steps once when a select sample’s tt values span one rank block, which on a chunk with dd bits a value means t⋅d≈st \cdot d \approx s. On the dense bitmap chunks of exact copies dd is about four or five bits, and t=64t = 64 needs ss near 256 to step once — where the block scan is 128 bits on average and the total reads 143. The settings that make the stepping cheap make the scan dear, and the ones that make the scan cheap make the stepping dear. A select sample spaced by bits rather than by values would hold the ratio fixed, but that is a rank sample, and the directory would be one level again.

The other sets

On divergence 0.01 the two-level directory reaches 74 bits read a select for 6.21 bits a value (a select sample every 64 values naming a rank block, rank samples every 64 bits), and within a budget of 100 bits read it costs 6.050 bits a value where the better single kind costs 6.178; outside about 65 to 100 bits read it lands on the single kinds' frontierOn divergence 0.01: bits a value against the mean bits a select reads, for the cheapest uncapped partition with select samples alone (every 256, 64, 16 values), rank samples alone (every 1024, 512, 256, 128, 64 bits), and both levels at t = 64, 128, 256 and s = 64, 128, 256. Two levels: t 64 s 64: 6.205 for 74 (1.62 rank samples stepped), t 64 s 128: 6.070 for 94 (1.05 rank samples stepped), t 64 s 256: 5.994 for 140 (0.81 rank samples stepped), t 128 s 64: 6.186 for 77 (2.38 rank samples stepped), t 128 s 128: 6.057 for 98 (1.39 rank samples stepped), t 128 s 256: 5.987 for 142 (0.85 rank samples stepped), t 256 s 64: 6.178 for 82 (3.08 rank samples stepped), t 256 s 128: 6.050 for 99 (1.95 rank samples stepped), t 256 s 256: 5.983 for 144 (1.19 rank samples stepped). Select samples: 5.917 for 931, 5.940 for 286, 6.052 for 107, 6.520 for 55. Rank samples: 5.917 for 931, 5.931 for 433, 5.947 for 253, 5.980 for 165, 6.049 for 104, 6.178 for 80.5.8066.206.4056789log₂ bits a select readsbits a valuet 64, s 64two levelsselect samples alonerank samples alonedivergence 0.01a model of reads, not a timing
Fig. 4 Divergence 0.01: the three families’ frontiers. Two levels at t 64, s 64: 6.205 bits a value for 74 bits read; at t 64, s 128: 6.070 for 94. Rank samples every 64 bits: 6.178 for 80; every 128, 6.049 for 104. Select samples every 16 values: 6.520 for 55; every 64, 6.052 for 107.

At a divergence of 0.01 the two-level directory reaches 74 bits read a select for 6.21 bits a value, and within a budget of 100 bits read it costs 6.050 bits a value where the better single kind costs 6.178; under 70 bits read it reaches nothing, where select samples every sixteen values read 55. The prediction there was under 70 for about 6.2, and the directory’s cheapest read misses it by four bits; at 80 bits read rank samples alone are as good, 6.178 against 6.186. At this divergence the run starts are spread over many small chunks, few chunks are long enough to carry select samples, and a two-level select is a rank-sample select that steps from the chunk’s first block instead of searching.

The second level pays only inside a narrow band of reads: within 70 or 90 bits read a select, the two-level directory saves 3 of 8 sets more than a twentieth of a bit a value over the better single kind — the scattered marks 0.49, exact copies 0.20, divergence 0.005 0.10 — and on the rest it saves nothing, or cannot read that little at allFor each set, the fewest bits a value within budgets of 70 and 90 bits read a select, for the two-level directory and for the better of select samples alone and rank samples alone, and the difference: the scattered marks: 70: 7.368 against 7.857, 90: 7.206 against 7.347; exact copies: 70: 3.734 against 3.929, 90: 3.533 against 3.718; divergence 0.0002: 70: 4.586 against 4.568, 90: 4.380 against 4.380; divergence 0.0005: 70: none against 5.721, 90: 5.363 against 5.363; divergence 0.001: 70: 6.174 against 6.174, 90: 6.079 against 6.048; divergence 0.002: 70: 6.675 against 6.675, 90: 6.651 against 6.651; divergence 0.005: 70: 6.857 against 6.846, 90: 6.745 against 6.846; divergence 0.01: 70: none against 6.520, 90: 6.178 against 6.178.within 70 bits readwithin 90 bits readthe scattered marks−0.489−0.141exact copies−0.195−0.185divergence 0.0002+0.017−0.000divergence 0.0005two levels cannot−0.000divergence 0.001−0.000+0.031divergence 0.002−0.000−0.000divergence 0.005+0.010−0.102divergence 0.01two levels cannot+0.000bits a value saveda model of reads
Fig. 5 For each of eight sets, the bits a value saved by the two-level directory over the better of select samples alone and rank samples alone, within 70 and within 90 bits read a select. Over a twentieth of a bit on three sets: the scattered marks 0.49 within 70 bits, exact copies 0.20 within 70, divergence 0.005 0.10 within 90. Nothing on the others, and on several neither the directory nor rank samples read so little.

The second level pays only inside a narrow band of reads: within 70 or 90 bits read a select, the two-level directory saves three of eight sets more than a twentieth of a bit a value over the better single kind — the scattered marks 0.49, exact copies 0.20, divergence 0.005 0.10 — and on the rest it saves nothing, or cannot read that little at all. The scattered marks are the surprise. They have no gathering, the partition puts them in a dozen chunks of a few hundred values, and there a select steps through 1.57 rank samples on average, close to the one the design wants; why the marks’ chunks step less than the copies’ was not separated, since their forms were not counted apart. The directory reads 61 bits a select there for 7.39 bits a value, where the cheapest single kind within 70 bits costs 7.86.

What one more level buys

The closing question of the earlier essay was whether a select’s two kinds of ignorance can each be removed by the directory built for it, or whether the cheapest directory is always one of them, chosen by where the long chunks are. The answer here is narrower than either. Two levels remove both kinds of ignorance only where the select samples’ spacing in values matches the rank samples’ spacing in bits, and on the dense chunks that matter most they do not match. In a band of reads around 60 to 100 bits a select, on the sets with long chunks, the two-level directory offers points neither kind alone reaches, a fifth to a half of a bit a value cheaper. Outside that band, or on sets of many short chunks, the cheapest directory is one of the two.

Where the sparse representation loses and sparse on average, dense where it gathers found the same structure in the partition itself: what is cheapest depends on the density of each stretch, not on the set’s average density. A directory is the same question one level down.

What the directory is made of

The two-level directory is cheap in bits because each of its levels is small. A rank sample on these chunks is about eight bits, the count of values before its block within a chunk of a few hundred values; a select sample is two to four bits, the number of a rank block within its chunk. At rank samples every 64 bits and select samples every 64 values, the directory adds about half a bit a value on exact copies — the difference between 3.27 bits a value with no directory and 3.76 with both levels, partition included — and of that the select samples are a small part. The rank samples are what costs, and they are what the reads at small budgets need.

The comparison with rank samples alone then comes down to one question: is it cheaper to binary-search the rank samples or to step through them from a block the select sample names? A binary search over EE samples reads about log⁡2(E+1)\log_2 (E+1) of them; stepping reads as many as there are blocks between the select sample’s value and the target. On a chunk with a few dozen rank blocks the search reads five or six, and stepping from a select sample every 64 values reads two or three, which is the band where the second level pays. The band is therefore set by a count of blocks, not by a count of bits read, and a collection whose long chunks held more or fewer rank blocks would move it: fewer blocks shrink the binary search until stepping has nothing to save, and more lengthen the steps until the search is cheaper again. Exact copies sit near the middle of that range, which is why they show the largest saving among the gathered sets. On short chunks with few blocks the search is already short, and on long sparse chunks the select samples span many blocks and stepping is long. A position split in two made the same kind of trade inside Elias–Fano itself, a cheap upper part read by arithmetic and a lower part read directly, and found it paid exactly where the two parts’ sizes were balanced.

What the locating apparatus becomes weighed this whole structure against the r-index it serves. Its run-start values are one of several sampled arrays the index holds, and a fifth of a bit a value on a few hundred run starts is a few dozen bits; whether the select reads matter more depends on how often the index locates, which the bits-a-value axis cannot show.

What the model does not count

Reads, not time. Every cost is bits read, a model in which a scanned bit and a sampled bit cost the same. The count is not the time is the standing warning: a rank sample read at an arbitrary address may cost a cache miss where a sequential scan of 64 bits costs nothing extra, and on a real machine the stepping would be dearer than this model says and the scan cheaper.

One text, copied. The sets are copies of one text at several divergences, and scattered marks. A collection whose long chunks had a different density would move the band where the second level pays.

Nine settings. Three values of each spacing. The ratio that sets the stepping suggests settings between them — ss = 192, or tt = 48 — none of which was run.

Still open: a select sample spaced by bits

The steps come from the select samples’ spacing being counted in values while the rank blocks are counted in bits, so that the number of blocks between a sample and its target depends on each chunk’s density. A directory could space its select samples per chunk, choosing for each chunk the tt that makes tt values span about one rank block — t≈s/dt \approx s/d for a chunk of dd bits a value — and charge the choice in the path as before.

The measurement that follows gives each chunk its own tt, from 8 to 512 values, set to the nearest power of two to s/ds/d, at ss = 64, 128 and 256, on all eight sets. The prediction is that the steps fall to about one a select on every set, that on exact copies the directory then reads under 70 bits a select for under 3.65 bits a value, and that the band where two levels pay widens to about 120 bits read. It could fail on the select samples’ own bits: a chunk dense enough to need t=8t = 8 carries a select sample every eight values, each several bits wide, and that may cost more than the steps it saves. The question is whether a select’s two directories can be matched to each other chunk by chunk, or whether matching them only moves the cost from the stepping to the samples.

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