What the machine does

Twenty bits apart

Two representations of one sparse set, six thousand seven hundred and forty-five bits against six thousand seven hundred and sixty-five. One exploits sparsity and the other exploits runs, and on this set at this density they price identically.

Two ways to store a bit vector that is not half ones, and they attack opposite ends of the density dial.

Elias–Fano stores the positions of the ones, split into a packed low part and a unary high part. Its cost is m(2 + log₂(n/m)) — proportional to the number of ones, with a logarithm of the sparsity.

Block classes store each block of b bits as the count of ones in it plus which block with that count it is. Its cost is Σ ⌈log₂ C(b, cᵢ)⌉ — the entropy of the block classes, which goes to zero on a vector of all zeros or all ones.

The first is small when the ones are few. The second is small when the bits are uniform, whichever value they take.

On the marks of a self-index, sweeping the sampling rate sweeps the density, and the two lines cross.

The sparse representation is not always the smaller oneThree representations of the marked rows of one 16,384-character index, against the sampling rate. The plain vector is flat, because it spends a bit a row whether the row is marked or not — that flatness is the whole reason the locating apparatus does not vanish as the sampling thins. Elias-Fano falls with the density and crosses the plain line at one in 4; at one in 1 it is 2.00x the plain vector, because a low width of zero puts every position in the high vector. Compressed blocks and Elias-Fano meet at one in 16, 20 bits apart: below that the blocks win and above it the split does.01e+42e+43e+44e+450100one sampled position in every …bitsplain, one bit a rowcompressed blocksElias-Fano16,384 rowscrosses at one in 4
Fig. 1 Three representations of the marked rows of one index against the sampling rate. The plain line is flat; the other two cross.

The crossing

At one sampled position in sixteen: Elias–Fano 6,745 bits, block classes 6,765.

Twenty bits apart on a set of a thousand and twenty-five marks in sixteen thousand three hundred and eighty-five positions.

Below that rate the blocks win and above it the split does. At one in eight: 11,406 against 10,023, blocks ahead by fourteen per cent. At one in thirty-two: 3,905 against 4,557, Elias–Fano ahead by seventeen.

So there is a genuine crossing inside the range a real index uses, and it is not at an end of the dial where nobody operates.

Twenty bits out of six thousand seven hundred is three parts in a thousand, which is well inside anything a reader should treat as a difference. It is worth reporting to that precision anyway, because the closeness is the finding: two structures built on unrelated principles, over one set, agreeing to a tenth of a per cent at one point of a dial they otherwise disagree about by factors.

A coincidence, and a useful one — it means the crossing is genuinely a crossing rather than a region where both are indifferent, and it means the decision at that rate can be made on something other than size.

Why they behave oppositely

The two representations are exploiting different properties and a set can have one without the other.

Elias–Fano exploits sparsity: it stores m things and its cost per thing is a logarithm of the sparsity, so a sparser set costs less in total and slightly more per element.

Block classes exploit uniformity within a block: a block of sixty-three bits containing one one has C(63, 1) = 63 possibilities, so its offset is six bits and its class is six, against sixty-three plain. A block containing zero ones has one possibility and costs its class alone.

At a density of one in sixteen a block of sixty-three holds about four ones, and C(63, 4) is 595,665, so the offset is twenty bits and the class six — twenty-six against sixty-three, a factor of 2.4.

At a density of one in a hundred and twenty-eight a block holds about half a one, so most blocks are empty and cost six bits each. That is 6/63 = a tenth of a bit per position, which is a factor of ten and is being beaten by Elias–Fano’s factor of fifteen.

The reason Elias–Fano pulls ahead is that the block representation’s cost has a floor: the class of every block, ⌈log₂(b+1)⌉ bits, is paid whether the block is empty or not. That is 6/63 of a bit per position, or n/10.5 bits total, and it does not shrink with the density.

A second flat term, on the other representation. At one in a hundred and twenty-eight the blocks cost 2,566 bits of which about 1,560 is class overhead on empty blocks.

Four prices for one set of marked rowsThe same 1,025 marked rows out of 16,385, held four ways, at a sampling rate of one in 16. The plain vector spends one bit a row and is 19,475 bits. Elias-Fano splits each position into a 3-bit low part and a high part written in unary, and is 6,745 — 34.6% of the plain vector. Compressed blocks reach 6,765, which at this density is close but not equal. The fourth row is not a structure: it is the accounting this collection has charged for Elias-Fano since the repetition strand, and it is 595 bits under the built object because it omits the directory a select needs.plain bit vector19,475compressed blocks6,76534.7%Elias–Fano6,74534.6%positions, priced as Elias–Fano6,15031.6%16,385 rows · one in 16 markedElias-Fano at 34.6%
Fig. 2 The four prices at the crossing, where the two compressing representations are within twenty bits of each other.
The split is chosen at the flat bottom of a shallow curveWhat each choice of low width costs, for 513 marks over 8,193 positions. The low parts cost m·w and the high vector costs m + n/2^w, so the total falls steeply while the high vector dominates and rises linearly once the low parts do. The textbook split is ⌊log₂(n/m)⌋ = 3, which costs 3,077 bits; rounding it up instead — which is what this collection's own size model has always done — gives 4 and costs 3,078. One bit apart on five thousand. That flatness is why an accounting written years before the structure prices it correctly, and it is a fact about the encoding rather than a coincidence: the minimum sits at 2^w ≈ n·ln2/m, within a factor of 1.44 of the split either way.02.5e+35e+37.5e+30246bits kept in the low partbits⌊log₂(n/m)⌋ = 3low partshigh vector513 marks in 8,193 positions1 bits between the two roundings
Fig. 3 The parameter behind one of the two curves: Elias–Fano’s split, chosen at the flat bottom of a shallow cost curve.

Both representations have a block-size-like parameter and only one of them has been swept in this collection. Elias–Fano’s is the low width and it is chosen by a closed form. The block representation’s is b, the block length, and rank is the only thing it does swept it and found an optimum near sixty-three for the vectors in a wavelet tree.

Whether sixty-three is the right b for a scattered sparse vector is a question nobody has asked, and it moves the crossing. A larger b reduces the per-block class overhead — the n-proportional floor — at the cost of longer offsets, so a block representation tuned for sparsity would push the crossing rightward.

The numbers here are at b = 63 throughout, which is the value this collection uses for the vectors it was tuned on. So the crossing is at one in sixteen for that block size, and a differently tuned block representation would cross elsewhere.

The operation counts, which the plate does not show

Since the size decision at the crossing has to be made on operations, it is worth putting the operation costs where a reader can compare them, because none of this strand’s plates does.

A plain vector’s rank is a superblock read, a block read and a scan of at most one block: three memory touches and a popcount, constant.

An Elias–Fano rank is a select0 on the high vector — which in this implementation is a binary search over the high vector’s superblock directory plus a scan — and then a walk of 1.25 entries. Several times a plain rank, dominated by the select.

A block-classed rank is a directory read and then a decode: reconstructing the block from its class and offset, which is an unranking of a combination and is the expensive part. Comparable to Elias–Fano’s, possibly worse, and not measured here.

So all three are constant-time and their constants span perhaps an order of magnitude, with plain fastest. That is the axis on which the decision at the crossing is made and it is the axis this collection has measured least.

The reason is that this strand’s instrument charges operations rather than time, and all three of these are one operation by that accounting. The comparison that is not one comparison is the essay about exactly that gap, and it applies here: three ranks costing three different amounts, all charged as one.

What decides at the crossing

Twenty bits is not a difference, so the choice at one in sixteen has to be made on something else, and there are three candidates.

The operations. A rank on Elias–Fano is a select0 on the high vector plus a bucket walk averaging 1.25 accesses. A rank on a block-classed vector is a directory read plus a decode of the block from its class and offset before counting — several times a plain vector’s work, and the decode is the expensive part. Elias–Fano wins.

The other operation. select1(k) on Elias–Fano is one select and one array read. On a block-classed vector it is a search over the directory and then a scan — much worse, because the representation is built around rank rather than select. Elias–Fano wins again, more decisively.

The construction. Both are linear. Elias–Fano is simpler by a wide margin: two arrays and a bit vector against a table of binomial coefficients and per-block encoding.

So at the crossing Elias–Fano is the choice on every axis except size, where the two are equal. That is a comfortable resolution and it makes the crossing less interesting than the plate suggests.

What the locating apparatus is, and what it becomesThe two locating parts of an FM-index over 16,384 characters, as a share of the whole structure, against the sampling rate. Plainly represented the share flattens at 18.5% however rarely a row is kept, because the marks are n bits and do not follow the rate at all. With the marks as an Elias-Fano array the share keeps falling, to 3.8% at one in 128. At one in 32 — the rate every published size in this collection is quoted at — the whole index goes from 104,720 bits to 89,150, a saving of 14.9% for a change to one array.02040255075100125one sampled position in every …of the index, per centplain marks: 18.5%Elias-Fano: 3.8%16,384 characters14.9% off at one in 32
Fig. 4 What the choice is a part of: the locating apparatus’s share of an index, against the rate, with the marks held two ways.

Why a crossing this close is unusual

Most crossings in this collection are between structures doing the same job with different constants, and they cross because one constant is larger. A crossing between structures exploiting different properties is a different kind of thing and is worth marking.

The collection decides which index is small has one: an entropy-bounded index against a run-length one, crossing because a collection has some predictability and some repetition and the mix decides. That crossing moves with the corpus and is a property of the data rather than of the structures.

This one is the same shape one level down. The marks have some sparsity and no runs, so the crossing sits where the sparsity’s value overtakes the runs’ absence — and on a set with runs it would move, and on a denser set it would move the other way.

The general property: a crossing between two structures exploiting one property is a fact about the structures; a crossing between structures exploiting different properties is a fact about the data. The first can be quoted; the second has to be re-measured on each object.

Twenty bits at one in sixteen is the second kind, and quoting it as a rule would be quoting a property of these marks.

Where the block representation genuinely wins

There is a regime where the ordering above reverses completely, and it is not on this plate’s right-hand side.

At density one — every row marked — the block representation costs 1,789 bits against the plain vector’s 19,475 and Elias–Fano’s 38,967. A factor of eleven better than plain and twenty-two better than the split.

That is because every block is all ones, so every offset costs nothing and the whole structure is its classes and directory.

The same holds at density zero and, less dramatically, at any density where the ones are clustered rather than scattered — a block that is all ones or all zeros costs nothing either way.

So the block representation’s real domain is vectors with runs, and a vector with m scattered ones is the case it handles least well. The marks are scattered by construction, because the transform sorts by context.

Rank is the only thing it does is where this collection priced the block representation, and it was on the levels of a wavelet tree over a transform — which have runs, which is the whole reason a compressed index is compressed. Applied to a scattered set it is being used outside what it is for.

What the marks would have to be

It is worth asking what set the block representation would win on here, because the answer says something about the sampling policy.

A vector with n/s ones arranged in runs — consecutive sampled rows — would be nearly free to the blocks and unchanged to Elias–Fano.

Sampling by row index rather than by suffix-array value gives exactly that: mark every s-th row and the vector is a regular pattern, which a block representation handles better than a scattered one at the same density. But sampling by row index does not support a locate, because a locate arrives at an arbitrary row and needs to know whether its position was sampled.

So the scattering is not incidental — it is what makes the sampling useful, and it is what makes the block representation the wrong tool. The sampling that goes the other way separated the two sampling directions in this collection and this is a consequence of that separation nobody had drawn.

One row in 16 is sampled, and the array saying which is n bitsThe 257 marked rows of an FM-index over 4,097 characters of english, one square per marked row, 128 rows to a line. The marks are the rows whose suffix-array value is a multiple of 16, and the transform scatters them — which is why the picture has no pattern in it and why a representation that exploits runs has little to work with. Stored plainly this is one bit for every row whether it is marked or not: 4,097 bits to record 257 numbers, which is 16x more bits than there are marks.4,097 rows · one in 16 marked6.3% of the rows
Fig. 5 The set at the crossing density: a thousand marked rows scattered by the transform, with no runs for a block representation to exploit.
Four prices for one set of marked rowsThe same 16,385 marked rows out of 16,385, held four ways, at a sampling rate of one in 1. The plain vector spends one bit a row and is 19,475 bits. Elias-Fano splits each position into a 0-bit low part and a high part written in unary, and is 38,967 — 200.1% of the plain vector. Compressed blocks reach 1,789, which at this density is close but not equal. The fourth row is not a structure: it is the accounting this collection has charged for Elias-Fano since the repetition strand, and it is 6,197 bits under the built object because it omits the directory a select needs.plain bit vector19,475compressed blocks1,7899.2%Elias–Fano38,967200.1%positions, priced as Elias–Fano32,770168.3%16,385 rows · one in 1 markedElias-Fano at 200.1%
Fig. 6 The density where the ordering reverses completely: every row marked, where the block representation is a tenth of the plain vector and Elias–Fano is twice it.

Two floors, on two representations

Both alternatives to the plain vector have a term that does not shrink, and putting them side by side is the tidiest way to see why the crossing exists.

Elias–Fano’s floor is 2m bits — one for the high vector’s one and one for its bucket boundary, roughly — plus its directory. That is proportional to m, so it does shrink with the density; it is a floor per element rather than per position.

The block representation’s floor is n·⌈log₂(b+1)⌉/b — the class of every block, paid whether the block is empty or not. At b = 63 that is n/10.5, proportional to n, and it does not shrink.

So at sparse densities the block representation has a term in n and Elias–Fano does not, which is the same shape as the plain vector’s n-bit payload one step milder. The crossing is where Elias–Fano’s per-element cost falls below the blocks’ per-position floor.

Solving: m(2 + log₂(n/m)) = n/10.5 gives n/m around fifteen, which is where the measurement puts it.

A price with no structure under itTwo objects that agree about space and disagree about time. The upper pair is bits: the size model charges 3,591 and the built Elias-Fano array costs 3,905, the difference being the directory a select needs — 8.0% of the structure, and exactly the omission that reporting a plain bit vector as n bits makes. The lower pair is one rank: the structure does a select on its high vector and then walks 0.70 entries of a bucket, because the low width is chosen so a bucket holds about one; the model does a binary search over 599 positions, which is 10 steps. The accounting was right and the object was not there.bits heldthe model3,591the structure3,905one rank, in operationsbinary search10 stepsselect and walk1 select + 0.7016,385 rows · one in 32 marked14x on the operation
Fig. 7 The comparison that had to be made before either could be trusted: an accounting against a built object, differing by a factor of eight on one operation.

What this says about a third representation

The two floors suggest an obvious hybrid and it is worth naming rather than building.

A block representation with a sparse index over non-empty blocks would not pay the class of an empty block — the empty blocks would simply not be listed, and their absence would be encoded by the index. That removes the n-proportional floor and leaves a structure that is small at both ends.

That is, roughly, what a production succinct library ships: a partitioned vector choosing a representation per block, with the choice encoded in a header. Its size is within a constant of the best of the three at every density.

This collection has not built it, for the reason where the sparse representation loses gives: an adaptive structure hides the boundary it adapts across, and measuring the boundary requires the non-adaptive versions. The crossing at one in sixteen is a number a hybrid’s implementation needs and its own measurements would never show.

The three representations as three assumptions

The cleanest way to hold this is that each representation is an assumption about the vector, and the assumption is what makes it small.

Plain. No assumption. n bits, always, and optimal for a vector of density a half. Every operation constant and cheap.

Elias–Fano. The ones are few. Cost m(2 + log₂(n/m)), which is the entropy of a sparse set plus half a bit an element. Select cheap, rank cheap-ish, and the assumption fails at density above about 0.3.

Block classes. The bits are locally uniform. Cost the entropy of the block classes, which is zero on constant blocks and maximal on random ones at density a half. Rank moderate, select poor, and the assumption fails on a scattered set.

A vector satisfying neither assumption should be plain. One satisfying both should be a hybrid. The marks satisfy the first and not the second, which is decided by the transform scattering them.

That framing also says what to measure when meeting a new bit vector: its density, and the entropy of its block classes. Two numbers, both a single pass, and between them they choose among the three.

One row in 4 is sampled, and the array saying which is n bitsThe 1,025 marked rows of an FM-index over 4,097 characters of english, one square per marked row, 128 rows to a line. The marks are the rows whose suffix-array value is a multiple of 4, and the transform scatters them — which is why the picture has no pattern in it and why a representation that exploits runs has little to work with. Stored plainly this is one bit for every row whether it is marked or not: 4,097 bits to record 1,025 numbers, which is 4.00x more bits than there are marks.4,097 rows · one in 4 marked25.0% of the rows
Fig. 8 The same set at four times the density, where neither assumption is satisfied strongly and the plain vector is close to the best of the three.

What the plain vector is still for

Ending without a word for the representation both alternatives are being compared against would leave the wrong impression, because it wins over part of the range and it wins on every operation everywhere.

At one row in four and denser, the plain vector is the smallest of the three — 19,475 bits against Elias–Fano’s 18,691 at exactly one in four, which is the crossing, and against 29,189 at one in two.

And at every density its operations are the cheapest: a rank is three memory touches and a popcount, with no decode, no select, and no walk.

So a system sampling densely should use it without hesitation, and a system sampling sparsely is trading operation cost for size — which is the same trade the sampling rate itself is, one level down.

That layering is worth noticing. The sampling rate trades index size against locate time. The mark representation trades index size against mark-lookup time. Both dials point the same way, and a system turning one should know it is compounding rather than choosing independently.

The array that says where is twice the samples is where the first dial’s effect on the second was measured, and what the locating apparatus becomes is where the two are put on one plate.

What to take from twenty bits

Two things, one narrow and one general.

For the marks: use Elias–Fano. The crossing is at one in sixteen, the sizes are equal there, and Elias–Fano wins on both operations and on construction simplicity. Below one in four use the plain vector, because both alternatives lose.

And: a crossing this close is a signal that the two structures are exploiting different properties of the same object, not competing at the same thing. Two representations that differ by twenty bits at one point and by a factor of twenty at the ends are not two implementations of one idea. They are two ideas, and the object happens to have a little of each.

That is worth knowing before optimising either. A structure with both sparsity and runs would want the hybrid; one with only sparsity wants the split; one with only runs wants the blocks. The marks have sparsity and no runs, which is why the answer is clean once the question is asked in those terms.

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.

Compressed bit vectorCrossing pointElias fanoIndex sizeRankSample marksSparse set