What a bound is

The flat bottom of a shallow curve

The low width is chosen as the floor of log of the universe over the count. Rounding it up instead costs one bit on five thousand, because the total is m·w plus n over two to the w and the minimum is where those two are equal.

Elias–Fano has exactly one parameter: how many of each position’s low bits to store packed, with the rest going into the high vector.

The textbook answer is w = ⌊log₂(n/m)⌋ and the reason is a calculation short enough to do in a line.

The low parts cost m·w. The high vector has m ones and n/2ʷ zeros, so it costs m + n·2⁻ʷ. The total is m·w + m + n·2⁻ʷ, whose derivative in w is mn·2⁻ʷ·ln 2 — zero at 2ʷ = n·ln 2/m.

So the optimum is at w = log₂(n/m) + log₂(ln 2), and log₂(ln 2) is −0.529. The textbook floor is within half a bit of the true optimum by construction, and the difference between the two is what this essay is about — which is to say, almost nothing.

The split is chosen at the flat bottom of a shallow curveWhat each choice of low width costs, for 1,025 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)⌋ = 2, which costs 5,124 bits; rounding it up instead — which is what this collection's own size model has always done — gives 3 and costs 5,125. 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)⌋ = 2low partshigh vector1,025 marks in 8,193 positions1 bits between the two roundings
Fig. 1 The total cost against each possible low width, with the low parts and the high vector drawn separately. The chosen width is marked.

The curve

For a thousand and twenty-five marks in eight thousand one hundred and ninety-three positions:

w = 0: low 0, high 9,219, total 9,219. w = 1: low 1,025, high 5,122, total 6,147. w = 2: low 2,050, high 3,074, total 5,124. w = 3: low 3,075, high 2,050, total 5,125. w = 4: low 4,100, high 1,538, total 5,638. w = 5: low 5,125, high 1,282, total 6,407. w = 6: low 6,150, high 1,154, total 7,304.

The minimum is at w = 2 and the next value is one bit away.

Reading the two component lines is worth a moment because they explain the shape better than the total does. The low parts are a straight line through the origin with slope m; the high vector is an exponential decay from n + m toward m. Their sum is a curve that falls steeply while the exponential dominates and rises linearly once the line does, and the transition between those two regimes is the minimum.

Everything about the curve’s shape follows from that, including its asymmetry: it is steeper on the left, because an exponential rising is faster than a line rising, so under-splitting is worse than over-splitting by the same amount. At w = 1 the total is 6,147 and at w = 3 it is 5,125, both one step from the optimum in opposite directions and a thousand bits apart.

Why the bottom is flat

The two terms move in opposite directions and at different rates, which is what makes the sum’s minimum shallow.

The low parts grow linearly in w: each extra bit costs m bits, always.

The high vector shrinks exponentially: each extra bit halves n/2ʷ.

Near the optimum those two rates are equal by definition, so moving w by one changes the total by (the linear cost of one bit) minus (half the current high vector), and near the optimum those are the same number. The first-order term cancels and what is left is second-order.

That is the general reason a sum of a linear and an exponential has a flat minimum, and it is why the choice is forgiving.

What the flatness excused

This collection has carried an Elias–Fano size model since the repetition strand — a class that reports r·low + 2r bits for a set of r positions — and it rounds the split up where the structure rounds it down.

On a set of m = n/8 the two disagree: the model splits at 3 and the structure at 2. On the curve above those are 5,125 and 5,124.

One bit in five thousand. The model has been pricing sets correctly, for several strands, using the wrong parameter — and nothing noticed, because the parameter it uses is one step from the one it should use on a curve whose steps near the bottom are single bits.

That is a comfortable outcome and it is worth being precise about what it does and does not say. The model’s width is wrong by one. Its total is right to a bit per thousand. And its omission of the high vector’s directory is a genuine error of ten per cent, which is a different matter and is a price with no structure under it.

So the flatness excused one of the model’s two problems and not the other.

The split is chosen at the flat bottom of a shallow curveWhat each choice of low width costs, for 257 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)⌋ = 4, which costs 1,798 bits; rounding it up instead — which is what this collection's own size model has always done — gives 5 and costs 1,799. 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.02e+34e+36e+38e+302468bits kept in the low partbits⌊log₂(n/m)⌋ = 4low partshigh vector257 marks in 8,193 positions1 bits between the two roundings
Fig. 2 The same curve for a sparser set, where the optimum moves right and the bottom is flatter still.

The flatness is not the same everywhere on the sampling dial, and the direction is convenient. A sparser set has a larger optimal w, and the curve near a larger w is flatter — because the exponential term is smaller there, so the second-order behaviour dominates over a wider range.

So a size model rounding the width the wrong way is most accurate on the sparse sets where the structure is most worth using, and least accurate on dense ones where it is not being used anyway.

Why the floor rather than the round

Given that the true optimum is at log₂(n/m) − 0.529, a reader might ask why the convention is the floor rather than rounding to nearest.

Two reasons, and only the second is about size.

The floor is what makes the bucket occupancy about one. The number of positions sharing a high part is m·2ʷ/n, which at w = ⌊log₂(n/m)⌋ is between a half and one. That is what bounds a rank’s scan, and it is the reason the choice is not purely a size question.

And rounding to nearest would sometimes give a larger w, which makes buckets emptier and the high vector longer — costing bits and a slightly longer select. The floor errs toward the side where the scan is bounded.

Four prices for one set of marked rowsThe same 513 marked rows out of 16,385, held four ways, at a sampling rate of one in 32. The plain vector spends one bit a row and is 19,475 bits. Elias-Fano splits each position into a 4-bit low part and a high part written in unary, and is 3,905 — 20.1% of the plain vector. Compressed blocks reach 4,557, 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 314 bits under the built object because it omits the directory a select needs.plain bit vector19,475compressed blocks4,55723.4%Elias–Fano3,90520.1%positions, priced as Elias–Fano3,59118.4%16,385 rows · one in 32 markedElias-Fano at 20.1%
Fig. 3 The structure the width decides the size of, against three alternatives for the same set.

What the curve looks like away from the bottom

The flatness is local and the curve is not flat elsewhere, which matters because a parameter chosen badly is caught by the size.

At w = 0 the total is 9,219 against the optimum’s 5,124 — a factor of 1.80, and every position is in the high vector, which is a unary encoding.

At w = 6, four above the optimum, it is 7,304 — a factor of 1.43, and the structure is approaching a packed array of positions.

So a wildly wrong width costs between forty and eighty per cent, which is enough to be noticed by any size measurement and not enough to be catastrophic. That is the shape of a well-behaved parameter: forgiving near the answer, mildly punishing far from it, and never wrong in kind.

The rejection test in this strand forces the degenerate case and requires the size check to catch it: at w = 0 on a sixteen-thousand-position set the structure is 10,043 bits against the chosen split’s 1,968, a factor of 5.1. That is a larger factor than the curve above shows because the set there is sparser — one row in thirty-two rather than one in eight — and the sparser the set, the more the high vector suffers from a small width.

What a rejection has to do here

A parameter whose wrong values cost only a size needs a rejection test that checks a size, and that is a weaker kind of check than most in this collection — so it is worth saying what it does and does not establish.

The test forces the degenerate split, rebuilds the high vector to match so that the object is a working structure with a badly chosen parameter rather than a broken one, and requires the size to be larger. It comes out at 10,043 bits against 1,968.

What that establishes is that the size accounting is sensitive to the parameter. What it does not establish is that the chosen parameter is the best one, because a check comparing two points cannot say anything about the rest of the curve.

The curve itself is the evidence for that, and a plate is not a check. So this strand’s position is: the width’s optimality is drawn and not asserted, and what is asserted is that a wrong width is caught.

That is the right division. Asserting an optimum would mean computing the whole curve at every check, which is the measurement rather than a guard; asserting that a specific bad value is caught is cheap and catches the failure that would actually happen, which is a width computed from the wrong quantity.

One row in 32 is sampled, and the array saying which is n bitsThe 129 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 32, 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 129 numbers, which is 32x more bits than there are marks.4,097 rows · one in 32 marked3.1% of the rows
Fig. 4 The set the width is chosen for, drawn: the marked rows of a real index, whose scattering is why the low bits are incompressible.

The parameter that is not a tuning knob

There is a difference between a parameter with an optimum and a parameter with a trade, and this one is the first kind. That is worth stating because most of the parameters in this collection are the second.

A sampling rate trades size against locate time, so there is no optimum — only a curve a system picks a point on. A compressed vector’s block size trades compression against decode cost. A cache’s size trades memory against hit rate.

The low width trades nothing. There is one number that minimises the size, it also happens to bound the rank’s scan, and moving away from it makes both worse. Nobody has to decide anything.

That is unusual enough to be worth noticing when it happens, because a parameter with an optimum can be computed at construction time and forgotten, and a parameter with a trade cannot.

A position split in two is the construction this parameter belongs to, and the array that says where is twice the samples is the term it is replacing. Both quote sizes that assume the width chosen here.

Where the second-order term goes

For completeness, the size at the optimum is worth writing because it is the number every other essay in this strand compares against.

Substituting 2ʷ = n·ln 2/m into m·w + m + n·2⁻ʷ gives m·log₂(n/m) + m·log₂(ln 2) + m + m/ln 2, which is m·log₂(n/m) + m(1 − 0.529 + 1.443) — that is, m·log₂(n/m) + 1.914·m.

The information-theoretic minimum for a sorted set of m elements in n is log₂ C(n, m) ≈ m·log₂(n/m) + 1.443·m.

So Elias–Fano is within 0.47 bits per element of the entropy of a sparse set, with constant-time select. That is the reason it is the standard representation and it is a bound rather than a measurement.

At m = 513 that gap is 241 bits against a structure of 3,905 — 6.2%, before the directory. The directory is 596 bits, which is another 15%. So the object is about a fifth over the information, and the encoding is six per cent over.

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. 5 Where the structure sits against its alternatives across the sampling dial, with the width chosen afresh at each point.
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. 6 The model’s other problem, which the flatness does not excuse: bits above, and the operation each object costs below.

The two roundings, side by side

It is worth putting the specific numbers down because they are the whole of the incident that produced this essay.

A set of 1,025 marks in 8,193 positions. n/m is 7.993 — just under eight, which is where the two roundings differ maximally.

⌊log₂ 7.993⌋ = 2. ⌈log₂ 7.993⌉ = 3.

At w = 2: low 2,050, high 3,074, total 5,124. At w = 3: low 3,075, high 2,050, total 5,125.

One bit.

That the set lands at n/m = 7.993 rather than 8.000 is itself worth a sentence: the index has 8,193 rows for 8,192 characters, because of the sentinel, and the marks are the rows whose suffix-array value is a multiple of eight — of which there are 1,025 rather than 1,024. Two off-by-ones in opposite directions putting the ratio a hundredth under a power of two, which is exactly where the floor and the ceiling disagree.

A test comparing the two roundings on a set whose n/m was 8.5 would have found them agreeing. This one found them differing, by one bit, which is the most informative possible outcome: the parameters differ and the answer does not.

Four prices for one set of marked rowsThe same 1,025 marked rows out of 8,193, held four ways, at a sampling rate of one in 8. The plain vector spends one bit a row and is 9,745 bits. Elias-Fano splits each position into a 2-bit low part and a high part written in unary, and is 5,720 — 58.7% of the plain vector. Compressed blocks reach 5,028, 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 vector9,745compressed blocks5,02851.6%Elias–Fano5,72058.7%positions, priced as Elias–Fano5,12552.6%8,193 rows · one in 8 markedElias-Fano at 58.7%
Fig. 7 The set the two roundings disagree about, priced four ways at the density where the disagreement happens.

What the flatness means for a reader

Two practical consequences and one general one.

A size model can round either way. A structure priced with ⌈log₂(n/m)⌉ rather than the floor is priced correctly to a bit per thousand. This collection’s own model does exactly that and has been right for several strands.

A width computed from stale parameters is nearly harmless. A structure built with w chosen for a set of a slightly different size — a common enough situation when the width is fixed at compile time — pays a second-order penalty rather than a first-order one.

And a parameter’s forgiveness is a property worth measuring. This collection has parameters that are not forgiving — a Bloom filter’s bit count, a Count–Min sketch’s width, a merge’s run length — where being off by a factor of two costs a factor of two. Knowing which kind a parameter is decides how much effort its tuning deserves, and the sweep that says so costs one plate.

Parameters that are not like this one

Setting this parameter beside the collection’s others makes the classification concrete, and the contrast is sharper than “some parameters are forgiving”.

A Bloom filter’s bit count. Halving it does not halve the false-positive rate; it raises it by a power. A filter allowed to be wrong draws that curve and it has no flat region at all.

A merge’s minimum run length. The sort the library ships measured a shipped value of thirty-two costing 39.6% on nearly sorted input at one size — a first-order penalty for a value that is right elsewhere.

A compressed vector’s block size. Rank is the only thing it does found an optimum and a curve either side of it, and the curve is shallow near the bottom for the same reason as this one — two terms moving oppositely — but its two terms are compression and directory rather than a line and an exponential, and the bottom is correspondingly less flat.

The pattern across those: a parameter balancing a linear cost against an exponential one has a very flat optimum; one balancing two linear costs has a corner; one appearing in an exponent has no flat region anywhere.

That is a rule of thumb rather than a theorem and it says where to spend tuning effort, which is the practical question a parameter raises.

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. 8 What the parameter’s structure is a part of: the locating apparatus as a share of an index, with the marks held two ways.

Why a model can be right for the wrong reason

There is a general lesson underneath the one-bit result and it is uncomfortable enough to be worth stating plainly.

A model that has been right for several strands was right by accident of the curve’s shape. Its width was wrong; the total it produced was right; nobody could have told the difference from its outputs.

That is a well-behaved failure — a wrong parameter on a flat curve produces a right answer — and it is exactly the kind of thing that stops being well-behaved when circumstances change. A model rounding up would go wrong the moment somebody used it on a set where the curve is not flat, which is a dense set, which is a case the model was never meant for and which nothing prevents.

So the correct response to “the model has been right anyway” is not relief. It is to check why it has been right, which is what this essay is, and then to decide whether the reason is stable. Here it is: the flatness is a property of the encoding rather than of the data, so the model will keep being right for as long as it is used on sparse sets.

That is a conclusion a reader can act on, and it took a curve to reach. Without it the available conclusions were “the model agrees, so it is fine” and “the widths differ, so it is broken”, and both are wrong.

A price with no structure under it is where the model’s other problem is measured, and the contrast between the two is instructive: one is a wrong parameter on a flat curve and costs nothing, and the other is a missing term and costs ten per cent.

The one thing the curve cannot say

The curve is about size and the strand’s other question is about time, and the width affects both.

A larger w means fewer, fuller buckets, so a rank’s scan is longer. A smaller w means more, emptier buckets, so the high vector is longer and its select’s search is over more bits.

At the chosen width the scan averages 1.25 accesses, and neither direction is measured here. The reason is that the size curve’s flatness means the interesting widths are all within one of each other, and the scan’s expected length changes by a factor of two across that range — from about 0.6 to about 2.5.

So a system that cared about the rank’s cost more than about a bit per thousand would choose w one below the size optimum, and the size would rise by one bit per thousand. That is a real option nobody would bother with, and it is the only sense in which this parameter has a trade at all.

The reason it is not worth bothering with is arithmetic rather than taste. A locate’s mark operations are a few per cent of its cost — the wavelet tree’s ranks dominate — so halving the bucket scan moves a locate by a fraction of a per cent, and the bit per thousand it costs is a fraction of a per cent of the structure. Two negligible quantities traded against each other.

Every occurrence at the same price is where a locate’s cost was broken down here, and it is the measurement that says which parts of this structure are worth tuning. The marks are not one of them.

What this makes readable

Essays that name this one as a prerequisite.

Named alongside this one

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

What links here

Every essay whose body links to this one.

The objects this essay names

Each one links to every other essay that touches it.

Bit vectorElias fanoIndex sizeOptimisationParameter choiceSample marksSparse set