What the machine does

Select is not rank backwards

Rank counts the ones before a position and select finds the position of the k-th one, and only the first has an obvious structure. The constant-time answer costs 1.56 bits per one, is bounded in a unit the machine does not charge for, and on a vector with one bit in fifty it inspects more positions than the binary search it replaced.

A bit vector of 65,536 positions holding 6,554 ones. Two questions about it.

How many ones are there before position ii? A directory of prefix counts, one per block of the vector, one per superblock above that, and a short scan of the remainder. The blocks are evenly spaced because positions are evenly spaced, so the scan is bounded by the block length, and the whole thing costs a fixed fraction of the vector. This collection built it two essays ago and the structure is obvious from the question.

Where is the kk-th one? There is no obvious structure at all, and the reason is one sentence: select’s answers are indexed by rank, and the ones are not evenly spaced. A million ones may sit in the first thousand positions or be strewn over the whole vector, so a fixed interval in kk maps to no fixed interval in position, and nothing about a block length bounds anything.

Bits of select support against positions inspected, at ten per cent densityA bit vector of 65,536 positions holding 6,554 ones and costing 77,979 bits with its rank directory. The upper curve keeps one position per L ones and scans from it: cheap, and its worst case grows with L. The lower flat curve is the dense-and-sparse structure, whose worst case is 7 ones passed at every sample interval, because the bound comes from the sub-block spacing and the interval only decides how many anchors are stored. Passing those 7 ones costs up to 186 position inspections at this density. Every one in the vector is asked for, so these are worst cases rather than the worst of a sample. Binary search over the rank directory costs no extra bits at all and inspects 510 positions. Both axes are logarithmic.1,00010,00010³bits of select supportpositions inspected, worst casebinary search, no extra bits: 510L=8L=256L=8L=256one position per L onesdense and sparse6,554 ones in 65,536 positions · sub-blocks of 8worst cases, every k
Fig. 1 Bits of extra structure against positions inspected in the worst case, over every one in the vector. The flat lower curve is the structure with two cases; the upper curve is a plain array of sampled positions; the dashed line is a binary search over rank’s own directory, which costs no extra bits at all.

Three answers, and the cheapest one is free

Binary search on the rank directory. The superblock counts are increasing, so the superblock holding the kk-th one is found by binary search, and the answer is found by scanning forward from its start. Zero extra bits. On this vector it scans up to 510 positions, passes 70 ones, and touches 16 machine words.

One position kept per LL ones. Store the position of the LL-th, 2L2L-th, 3L3L-th one outright, and scan forward from the nearest. At L=8L=8 that is 13,120 bits — two bits per one — and the scan passes at most 7 ones.

Dense and sparse superblocks. The structure everything ships. Keep the anchors as above; then, for each stretch between anchors, decide which of two cases it is in:

  • if the LL ones between two anchors are spread over more than a threshold of positions, write out all LL answers. Call it sparse. There cannot be many such stretches, because each consumes a threshold’s worth of a vector that only has nn positions, and that argument is what bounds the space;
  • otherwise the LL ones sit within the threshold, so an offset into the stretch fits in log2threshold\lceil \log_2 \text{threshold}\rceil bits, and one offset per MM ones bounds the scan at MM ones.

The two cases are the wrong way round

Which case a stretch falls into depends on how far apart its ones are, not on how many there are. So:

ones select support sparse stretches of which written out
ones spread over the whole vector 1,966 31,952 bits 31 31,456 bits
ones packed into a tenth of it 1,966 2,956 bits 0 0
The same number of ones, two distributions, and the sparse case is the spread one1,966 ones in 65,536 positions, once spread evenly over the whole vector and once packed into the first tenth of it. The spread vector's superblocks each cover about 2133 positions, more than the 1024-position threshold, so 31 of them are sparse and every one of their answers is written out in full: 31,456 bits. The packed vector has none, and its whole support is 2,956 bits against 31,952. A structure whose size depends on where the ones are, rather than on how many, is the shape of thing an average-case analysis cannot describe.1,966 ones in 65,536 positions, two distributionsspread — superblock anchors496spread — sparse answers31,456spread — dense offsets0packed — superblock anchors496packed — sparse answers0packed — dense offsets2,4601,966 ones · threshold 1024 positions31,952 against 2,956 bits
Fig. 2 The same number of ones in the same length of vector, drawn once spread evenly and once packed into a tenth. The spread vector is the one with sparse superblocks and it costs eleven times as much, nearly all of it the answers written out in full.

The sparse case is the vector with the ones spread out. That is the opposite of what the word invites, and it is worth saying plainly because it inverts the intuition about which vectors are expensive: a structure whose size depends on where the ones are, rather than on how many, costs the most on the vector with the fewest.

It also means the size is not a function of nn and the number of ones. Two vectors with identical shape parameters differ here by a factor of eleven, and no average-case description of the structure distinguishes them.

What “constant time” is constant in

The guarantee is that a query passes fewer than MM ones. On this vector at M=8M = 8 it passes 7, and it passes 7 at every anchor interval — the bound comes from the sub-block spacing and LL only decides how many anchors are stored.

Passing 7 ones means inspecting whatever number of positions holds 7 ones, which at a tenth density is about 70 and at worst 186.

One worst case in three units, and only one of them is bounded by the parameterA vector of 65,536 positions at 10 per cent density. The structure's guarantee is that a query passes fewer than M = 8 ones, and it does: 7. Passing them costs 186 position inspections, because at this density the ones are ten apart — so the operation is constant in the unit its analysis is written in and linear in the reciprocal of the density in the unit a machine charges. The third row of each group is what closes the gap in practice: 7 machine words of 32 bits, which is what a population count instruction turns those 186 inspections into.worst case over every one in a vector of 65,536binary search — ones passed70positions inspected510machine words touched16one position per 64 — ones passed63positions inspected790machine words touched26dense and sparse, M = 8 — ones passed7positions inspected186machine words touched7worst case over every one in the vector · words of 32 bitsdensity 10%
Fig. 3 One worst case in three units, for the three strategies. Only the third row of each group is what a machine charges for, and only the first is what the analysis bounds.
strategy ones passed positions inspected machine words extra bits
binary search 70 510 16 0
one per 8 ones 7 186 7 13,120
dense and sparse, M=8M=8 7 186 7 22,960

So the operation is constant in ones and linear in the reciprocal of the density in positions, and those are the same number only on a vector that is half ones. Every published statement of the bound is in the first unit; every implementation’s cost is in the third.

The third unit is what closes the gap, and it is why every real implementation of this is written against a population count. Inspecting sixty-four positions is one machine operation, so the seven words above are seven operations rather than a hundred and eighty-six — which this collection has measured directly in another setting and which is the difference between a structure that is theoretically constant and one that is practically fast.

Tightening MM tightens the bound and costs bits, which is what makes it a trade rather than a parameter:

MM ones passed positions words bits of support
4 3 93 4 10,764
8 7 146 6 5,772
16 15 227 9 3,276

Where the structure with the guarantee loses

The comparison that decides whether the machinery is worth building is against the plain array of sampled positions, and it is not a rout.

structure bits bits per one ones passed positions
one position per 8 ones 13,120 2.00 7 186
dense and sparse, L=8L = 8 22,960 3.50 7 186
dense and sparse, L=64L = 64 11,488 1.75 7 186
dense and sparse, L=256L = 256 10,256 1.56 7 186
one position per 256 ones 416 0.06 255 2,825

At L=8L = 8 the two-case structure gives exactly the bound the plain array gives and costs 75% more bits. It only starts winning once its anchor interval is eight times the plain array’s — which is the whole reason the anchors are separated from the sub-block offsets in the first place, and it is a fact about the construction that the description “constant time in o(n)o(n) bits” contains no trace of.

And on a sparse vector it loses outright. Hold everything and turn the density down:

density ones ones passed positions words bits
0.40 26,214 7 43 3 45,884
0.10 6,554 7 186 7 11,488
0.02 1,311 7 831 27 2,304

The binary search inspects at most 510 positions and 16 words on any of these, for no bits at all.

At one bit in fifty, the structure with the constant-time guarantee inspects 831 positions to the binary search’s 510, touches nearly twice the machine words, and costs 2,304 bits the binary search does not spend. It is better in exactly one unit — the one the theorem is written in — and worse in every unit a processor charges for.

The mechanism is the asymmetry the essay opened with, arriving from the other side. Rank’s directory is indexed by position, so its scan is bounded in positions. Select’s is indexed by rank, so its scan is bounded in ones, and converting that bound into positions requires dividing by a density the structure does not control. At high density the conversion is favourable and at low density it is not, and the crossover on this vector is somewhere around one bit in twenty.

The construction, walked once

It is worth walking a query through the structure, because the two cases are easy to describe and easy to get wrong, and one of the ways to get them wrong is the rejection this essay’s machinery carries.

Ask for the position of the kk-th one. Divide kk by LL to get the stretch it falls in and the offset within that stretch — one integer division, no search. Look the stretch up in the sparse table; if it is there, the answer is written down and the query is over in two array reads. Otherwise divide the offset by MM to get a sub-block, read the offset of that sub-block’s first one from the dense table, add it to the stretch’s anchor, and scan forward past at most M1M-1 ones.

Nothing in that walk is a search. Everything is an index computed by arithmetic from kk, which is what makes the whole thing constant in the analysis’s unit — and the reason it is possible at all is that the anchors re-impose regularity on a sequence that had none. A stretch is LL ones wide by construction, so “which stretch” is a division; what varies is how many positions a stretch covers, and the two cases exist precisely to handle both regimes of that.

The failure this admits is silent, and it is the one worth a rejection. Every table above is built once from the vector and none of them holds a copy of it. Change a single bit in the vector afterwards and nothing throws, nothing is out of range, and no query reports an error — select simply returns a position that now holds a zero. The check carried beside this structure clears the bit that a query has just returned and requires the next identical query to answer somewhere that is not a one. It is the same shape as a stale rank directory, and it is the reason both structures are built from an immutable vector in this collection rather than maintained.

The second rejection is smaller and is about the parameters. LL must be a whole number of sub-blocks, because the dense table stores one offset per MM ones within a stretch of LL; an LL that is not a multiple of MM leaves a final partial sub-block whose scan is unbounded, and the bound is then quietly wrong rather than absent. That is refused at construction.

Why the structure exists anyway

None of that makes select support useless, and it is worth being precise about where it earns its bits, because the answer is not “when the vector is dense”.

It earns them when the query is on a hot path. A binary search over the superblock counts is a handful of dependent memory accesses, each a potential cache miss, and the dependency chain cannot be broken by a wider machine. The two-case structure’s lookup is two independent array reads followed by a short local scan, and locality is the whole of the difference — this collection has measured that distinction elsewhere and it is worth more than the operation counts suggest.

It earns them when a bound is required rather than an average. The binary search’s 510 positions is a worst case over this vector, and a different vector gives a different number; the two-case structure’s bound in ones holds by construction on every vector.

And it earns them where select is called once per output character, which is where it actually appears: walking a wavelet tree, decoding a compressed sequence, iterating the set bits of an index. A structure queried once does not need any of this. A structure queried nn times does, and the bits are amortised over the calls rather than over the vector.

Bits of select support against positions inspected, at ten per cent densityA bit vector of 65,536 positions holding 1,311 ones and costing 77,979 bits with its rank directory. The upper curve keeps one position per L ones and scans from it: cheap, and its worst case grows with L. The lower flat curve is the dense-and-sparse structure, whose worst case is 7 ones passed at every sample interval, because the bound comes from the sub-block spacing and the interval only decides how many anchors are stored. Passing those 7 ones costs up to 831 position inspections at this density. Every one in the vector is asked for, so these are worst cases rather than the worst of a sample. Binary search over the rank directory costs no extra bits at all and inspects 510 positions. Both axes are logarithmic.1001,00010,00010³10⁴bits of select supportpositions inspected, worst casebinary search, no extra bits: 510L=8L=256L=8L=256one position per L onesdense and sparse1,311 ones in 65,536 positions · sub-blocks of 8a plain sampling at L=8 matches it for 2,624 bits
Fig. 4 The same trade at one bit in fifty. The structure with the guarantee sits above and to the right of the free binary search on both axes; at this density the machinery is a cost with nothing on the other side of it.
Bits of select support against positions inspected, at ten per cent densityA bit vector of 65,536 positions holding 26,214 ones and costing 77,979 bits with its rank directory. The upper curve keeps one position per L ones and scans from it: cheap, and its worst case grows with L. The lower flat curve is the dense-and-sparse structure, whose worst case is 7 ones passed at every sample interval, because the bound comes from the sub-block spacing and the interval only decides how many anchors are stored. Passing those 7 ones costs up to 43 position inspections at this density. Every one in the vector is asked for, so these are worst cases rather than the worst of a sample. Binary search over the rank directory costs no extra bits at all and inspects 511 positions. Both axes are logarithmic.10,000100bits of select supportpositions inspected, worst casebinary search, no extra bits: 511L=8L=256L=8L=256one position per L onesdense and sparse26,214 ones in 65,536 positions · sub-blocks of 8worst cases, every k
Fig. 5 And at four bits in ten, where the conversion from ones to positions is nearly free and the guarantee is worth having. The same structure, the same parameters, the opposite conclusion.

One consequence of that locality is worth separating from the rest, because it is the practical reading. A structure whose cost cannot be predicted from a density is a structure that has to be built before it can be budgeted for, and building it requires the vector, which requires the data. Every other size in this family — the payload, rank’s directory, the sample marks — is a function of the vector’s length and a parameter, and can be worked out on paper before anything exists. Select support is the one line in the budget that has to wait for the data to show up, and a design that has committed to a memory footprint before then has committed to a number it cannot yet compute.

What the vector costs beside it

The support is measured against the vector it supports, which is this family’s standing rule.

bits
the vector’s payload 65,536
rank’s directory 12,443
select support, L=256L=256, M=8M=8 10,256

Thirteen per cent of the vector, to answer a question the vector could already answer with a binary search over a directory that was there for something else. Whether that is a good trade is a question about how often the question is asked, and it is not a question the size alone can settle — which is why every plate in this family prints what the thing being supported costs beside what the support costs.

The rank directory: overhead against the work of one rankThe directory over a bit vector of 8,192 bits, as a percentage of the payload, against the length of a block. A block of 8 costs 163% overhead and reads 3 words a query; a block of 512 costs 3.2% and reads 18. "Constant time and o(n) extra space" is true of every point on this plot and says nothing about which one to build, and the two numbers that decide it are typed into a header by a person.1010010100bits in a blockdirectory, % of the payloaddirectory, %words readbit vector of 8,192 bits · superblock 4,096, word 32dashed: words per rank
Fig. 6 Rank’s own directory under the same accounting, from the essay that built it: a longer block costs less directory and more reads for one query. Select’s structure has the same shape of trade with the two quantities in different units, which is the whole of what makes it harder to describe.

The zeros need their own structure

Everything above answers “where is the kk-th one”. A great many uses need the other question — where is the kk-th zero — and the structure just built answers it not at all.

Symmetry makes that easy to underrate. Select over the zeros is the same construction over the complemented vector, so it is the same code and the same parameters, and a reader may reasonably assume it is the same cost. It is not, and the reason is the inversion two sections above: the size depends on how far apart the selected bits are, and complementing a vector changes that completely.

On the vector this essay measures — 65,536 positions, one bit in ten — the ones are spread thinly and the zeros are packed nine to ten. So select over the ones meets the expensive case, and select over the zeros meets the cheap one. The two structures answering mirror-image questions about one vector differ in size by roughly the ratio of the densities, and the expensive one is always the sparser bit.

That matters because the structures that need select need both directions. Walking a wavelet tree upwards from a leaf follows a zero at some levels and a one at others — which branch was taken is what the code says — so a tree with select support has both structures at every node, and the node’s total is the sum of a cheap one and an expensive one rather than twice either. The accounting a reader would do from the table above, doubling the select row, is wrong in a direction that flatters the structure at high densities and understates it at low ones.

There is one consolation and it is worth stating because it is the only place in this essay where the sparse case pays for itself. A vector’s ones and zeros cannot both be sparse. Whichever direction is expensive, the other is cheap, so the sum of the two supports is far better behaved than either — it is largest at a density of a half, where both are moderate, rather than at the extremes where the naive reading would put it.

The case is decided locally, which the totals hide

The eleven-to-one table is the essay’s most striking number and it is a worst case, because it was measured on a vector whose ones are spread evenly everywhere. Real vectors are not like that, and the structure is better than the table suggests for a reason worth naming.

The sparse-or-dense decision is taken per stretch of LL ones, not once for the vector. A stretch whose ones are packed gets an offset table costing a few bits each; a stretch whose ones are spread gets its answers written out. So a vector with a dense head and a thin tail pays the cheap rate over the head and the expensive rate over the tail, in proportion to how many of its ones live in each, and neither region’s cost is contaminated by the other’s.

The structure is therefore adaptive at a granularity no summary statistic of the vector reports. Two vectors with the same length, the same number of ones and the same variance of gap lengths can differ by a large factor here, because what decides the cost is how the wide gaps are clustered — many wide gaps inside a few stretches is cheap, the same wide gaps spread one per stretch is expensive.

That is an unusual shape for a space bound and it is the same one the compressed index’s blocks have: a per-block encoding whose total is a sum over local decisions, so the cost tracks local structure and no global parameter predicts it. In both cases the practical consequence is identical and slightly uncomfortable. The size has to be measured on the actual vector, a size quoted for a density is a size quoted for one arrangement at that density, and the only defensible way to report either structure is to build it over the data in hand and weigh it.

What is not measured here

A true worst-case constant. The scheme here is the two-level one everybody ships, whose scan is bounded by a parameter. The three-level scheme that achieves a worst case bounded by a universal constant replaces the sub-block scan with a table indexed by a machine word — which is still a constant chosen by the word size, and is the honest form of the claim in every case.

Select on a compressed vector. The vector here stores its bits. A vector stored as a class and an offset per block, which is what makes a compressed index compressed, needs a different construction, and its select is not this one with different constants.

A clock. Every number above is a position inspected, a one passed, a machine word touched, or a bit retained. Which of them dominates on a particular processor is exactly the question this collection refuses to answer from a count, and the three-unit table is the closest it gets.

One worst case in three units, and only one of them is bounded by the parameterA vector of 65,536 positions at 2 per cent density. The structure's guarantee is that a query passes fewer than M = 8 ones, and it does: 7. Passing them costs 831 position inspections, because at this density the ones are ten apart — so the operation is constant in the unit its analysis is written in and linear in the reciprocal of the density in the unit a machine charges. The third row of each group is what closes the gap in practice: 27 machine words of 32 bits, which is what a population count instruction turns those 831 inspections into.worst case over every one in a vector of 65,536binary search — ones passed19positions inspected510machine words touched16one position per 64 — ones passed63positions inspected3,873machine words touched122dense and sparse, M = 8 — ones passed7positions inspected831machine words touched27worst case over every one in the vector · words of 32 bitsdensity 2%
Fig. 7 The three units at one bit in fifty, where the row that is bounded and the row that is charged for point in opposite directions. This is the plate the phrase “constant-time select” needs printed beside it.
The same number of ones, two distributions, and the sparse case is the spread one983 ones in 32,768 positions, once spread evenly over the whole vector and once packed into the first tenth of it. The spread vector's superblocks each cover about 2133 positions, more than the 1024-position threshold, so 15 of them are sparse and every one of their answers is written out in full: 14,400 bits. The packed vector has none, and its whole support is 1,470 bits against 14,670. A structure whose size depends on where the ones are, rather than on how many, is the shape of thing an average-case analysis cannot describe.983 ones in 32,768 positions, two distributionsspread — superblock anchors240spread — sparse answers14,400spread — dense offsets30packed — superblock anchors240packed — sparse answers0packed — dense offsets1,230983 ones · threshold 1024 positions14,670 against 1,470 bits
Fig. 8 The two distributions again on a vector half the length, where the same thirty-one sparse superblocks arrive at half the threshold. What decides the case is the ratio of the span to the threshold, and both halves of that ratio are parameters.

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

The 8 essays that link to this one and share the most of its objects, of 9 that link here.

The objects this essay names

Each one links to every other essay that touches it.

Bit vectorConstant timeDensityDirectoryMachine wordMeasurementRankSelectSpace overheadSuccinct structureTrade offWorst case