What is taught wrongly

A price with no structure under it

A class in this collection has charged Elias–Fano's price for several strands and stores an array of positions searched by binary search. The accounting is right about space to a bit per thousand and wrong about one operation by a factor of eight.

This collection has a class that represents a sparse set of positions. Its bits() method returns r·low + 2r, which is Elias–Fano’s price. It has been used to size several structures across several strands and every number it produced has been reported as a size.

What it stores is an Int32Array of the positions. What it does for a rank is a binary search over that array.

It is a size model with no structure under it — an accounting for an object that does not exist anywhere in the code — and the two ways it differs from a real Elias–Fano array had never been checked, because nothing had built one to check against.

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. 1 Two objects that agree about bits and disagree about one operation. The upper pair is what each holds; the lower pair is what one rank costs.

What it is right about

The payload. On four sampling rates the model’s total and the structure’s payload agree to within a bit per thousand: 5,125 against 5,124, 3,078 against 3,077, 1,799 against 1,798, 1,032 against 1,031.

That agreement is not luck and it is not a coincidence either. It is the flat bottom of a shallow curve: the model splits at ⌈log₂(n/m)⌉ and the structure at the floor, and near the optimum the cost curve’s steps are single bits.

So a wrong parameter on a flat curve produced a right answer, for several strands, and nothing could have noticed from the outputs.

What it is wrong about, in bits

The model charges r·low + 2r and calls the first term the payload.

The structure costs m·w for the low parts plus the high vector in full — its payload and its directory, because a select on it needs the directory and a structure whose select does not work is not a structure.

That directory is 596 bits at m = 513 in n = 16,385, against a structure of 3,905. The model omits 10.4%.

Across four rates the omission is 10.4%, 9.3%, 8.6%, 8.7% — stable, because the directory is a function of the vector’s length and the vector’s length is proportional to the payload.

Ten per cent is not enormous and it is the same omission as reporting a plain bit vector as n bits, which this collection has been careful about since an index larger than what it indexes. It is a real error in a number that has been used.

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. 2 Why the two agree about space: the cost curve against the low width, whose bottom is flat enough that the model’s rounding and the structure’s differ by one bit in five thousand.

There is a temptation to describe the omission as “the model reports the payload and the structure reports the total”, which would make it a difference in convention rather than an error. It is not, and the distinction matters.

A payload is a meaningful quantity when the directory is separately accounted for somewhere. In this collection’s own accounting for a plain bit vector, bits() returns {payload, directory, total} and every caller that sums them uses the total. The model returns one number and calls it a total, and that number is the payload.

So the omission is not a convention; it is a number labelled as something it is not, being summed with numbers that are what they say. An index larger than what it indexes is where this collection started insisting that a reported size say what is in it, and this is a case of a size that said the wrong thing quietly.

What it is wrong about, in operations

This is the part no measurement of bits could have found.

The structure’s rank1 is a select0 on the high vector to find the bucket, then a walk along the bucket comparing low parts. The bucket’s expected occupancy is one by construction, and the measured walk is 1.25 accesses.

The model’s rank1 is a binary search over m positions: ⌈log₂ 513⌉ = 10 steps.

A factor of eight on the operation, between two objects that price identically.

That is not an implementation detail of the model. It is what the model is: an array of positions is a perfectly good sparse-set representation whose rank is a search, and Elias–Fano is a different representation whose rank is a select and a constant walk. The model has the second’s price and the first’s behaviour.

Why the check has to be on operations

Every check available in this strand before this one was a size check, and none of them could separate the two objects.

They report the same payload. They answer the same ranks with the same values. They accept the same sets. They are indistinguishable by any test of their outputs and by any measurement of their space to within ten per cent.

What separates them is a number that neither returns: how many steps a rank took.

So the check is an inequality on operation counts, and it is written to require the two to differ — a check requiring them to agree would be a check that a size model is a structure.

Measured over four hundred probes: 1.25 against 10, a ratio of 8.0.

The other half of the check

An inequality between two objects’ costs can pass for the wrong reason — the counters could be charging different things — so the rejection test has a second half.

Two copies of the same structure, over the same set, probed identically, must report the same cost. If they did not, the counter would be measuring something other than the structure.

They do: identical access counts over two hundred probes.

So the check says two things together. The structure and the model differ, and two copies of the structure do not. Without the second, a difference between the two could be noise in the instrument rather than a property of the objects.

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 four prices for one set, with the model as the fourth row: an accounting rather than an object, and the only one of the four with nothing behind it.
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 The structure the corrected model prices a part of: an index’s locating apparatus against the sampling rate, with the marks held two ways.

The factor of eight also moves with the set’s size, which the size error does not. A binary search is ⌈log₂ m⌉ steps, so on a set of eight thousand marks it is thirteen and on a set of five hundred it is ten. The structure’s bucket walk is 1.25 regardless — it depends on the density, which the low width holds at about one per bucket, and not on m.

So the gap grows logarithmically with the collection. On a million-row index sampling one in thirty-two, the model’s rank is fifteen steps and the structure’s is 1.25, a factor of twelve.

That is worth knowing because it is the direction that makes the two harder to tell apart from their sizes and easier to tell apart from their behaviour, which is the opposite of what an accounting error usually does.

Why a model survives

It is worth asking how an accounting for a nonexistent object stays in a codebase across several strands, because the answer is not carelessness.

It was never wrong about the thing it was used for. Every use of the class was a size, and the size was right to a bit per thousand. A model that is right about its use is a model nothing complains about.

Its behaviour was never on a plate. The class’s rank1 was called by measurement code that did not count operations, so the binary search never appeared in a figure. What is not drawn is not noticed.

And there was nothing to compare it against. Elias–Fano did not exist in this collection until this strand. A model of a structure that has not been built cannot be checked against the structure, and the only available check is arithmetic — which it passes.

That third one is the general condition. A model is checkable when the thing it models exists; before that it is a formula, and a formula’s correctness is a matter of derivation rather than of measurement.

The general shape

Two failure modes are being conflated in ordinary usage and this incident separates them.

A model that is wrong. Its formula does not describe the object. Caught by building the object and comparing.

A model that is right and is not an object. Its formula describes the object correctly and the code holding the formula behaves differently. Caught only by measuring the code’s behaviour, which nobody does to a model, because a model is understood to be a formula.

The second is the one here, and it arises specifically when a model is implemented as a class with methods. A model that were a function returning a number could not have this problem: nobody would call it to answer a rank.

That suggests a rule with some content: a size model should not have the interface of the thing it models. A class named SparseBits with rank1, select1 and bits invites being used as a sparse bit vector, and it is a size formula wearing a structure’s clothes.

Whether the fix is to rename it, to remove its query methods, or to replace it with the real structure is a judgement about what else uses it. What is not in doubt is that the two objects have been reported as one.

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 model’s numbers have been used: the sizes of three representations across the sampling dial, one of which was priced by the model for several strands.

Three objects this collection has caught being one

The pattern has now appeared three times in this collection, on three different kinds of object, and putting them together makes the class of defect legible.

A structure that still holds what it claims to have dropped. What the chain cost had to guard against an index that kept a previous-occurrence chain and promised not to read it — which has the same size as one that uses it. The guard is to make the object raise when asked, so that a structure claiming not to have something cannot answer as though it did.

A generator called through the wrong path. A registry built twice produces two module instances and two caches; both are correct, and the thing being measured is not the thing being shipped. Caught by making the registry a module rather than a value the configuration exports.

A price with no structure under it. This one. Caught by counting operations on both.

All three are cases where two objects are reported as one because the reporting instrument cannot separate them. The instruments differ — an exception, a module identity, an operation count — and the shape is identical: the check has to be on a property the two objects do not share, and the properties they do share are the ones anybody was looking at.

That is the useful abstraction. When two things are being treated as one, ask what they do not have in common, and check that.

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. 6 The set both objects represent: the marked rows of a real index, which neither of them stores as a bit vector.

What a correct model would look like

Since the model is staying — several structures are sized through it and replacing every call site is more churn than the ten per cent justifies — it is worth writing what its formula should be.

The low parts: m·⌊log₂(n/m)⌋, with the floor rather than the ceiling. That is a bit per thousand and it is free to fix.

The high vector’s payload: m + n/2^w, which the model approximates as 2m. At the chosen width those are within a factor of about 1.4 of each other, and the exact form costs nothing to write.

The high vector’s directory: whatever the implementation’s rank structure costs, which for a two-level directory over a vector of length L is about L/b·log₂ L + L/sb·log₂ L bits. On this collection’s parameters that is the 8–10% measured.

Three terms rather than two, one of which depends on the implementation’s block sizes. That last dependence is the reason the model was written without it — a size model that has to know the directory’s parameters is a size model coupled to an implementation — and it is also the reason the omission is not a rounding.

The resolution this strand takes is to keep the model, correct its width, and have the structure report the total wherever a total is wanted. A model is then a model of the encoding and the object is the authority on the object, which is the division that should have existed from the start.

Four prices for one set of marked rowsThe same 257 marked rows out of 16,385, held four ways, at a sampling rate of one in 64. The plain vector spends one bit a row and is 19,475 bits. Elias-Fano splits each position into a 5-bit low part and a high part written in unary, and is 2,225 — 11.4% of the plain vector. Compressed blocks reach 3,268, 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 169 bits under the built object because it omits the directory a select needs.plain bit vector19,475compressed blocks3,26816.8%Elias–Fano2,22511.4%positions, priced as Elias–Fano2,05610.6%16,385 rows · one in 64 markedElias-Fano at 11.4%
Fig. 7 The same four prices at a sparser sampling, where the directory’s share of the structure is largest and the model’s omission correspondingly worst.

What has to be re-read

The honest consequence is that every size this collection has quoted through that class is ten per cent low, and it is worth saying which those are.

The r-index’s predecessor structure over the phi anchors, which is r positions in n — sized by this class since a function with r pieces built it.

The run-length index’s run starts, and the per-symbol run-end vectors, which are the same shape and are what the index that stores the runs is priced from.

Each of those is one part of a larger structure, so the ten per cent is on a part rather than on a total. Whether it changes any conclusion drawn from those sizes is a question this strand does not chase, and the answer is almost certainly no: the comparisons those numbers appeared in were between structures both of which used the class.

That is the mild version of a bad situation. A systematic error in a component appearing on both sides of every comparison cancels, and the numbers are wrong in a way that does not move any of the orderings they were used to establish.

What building the structure was actually for

There is a reading of this strand in which the Elias–Fano array was built to save bits and the model’s exposure was a by-product. The reverse is closer to what happened and it is worth recording.

The array was built because the array that says where is twice the samples found an n-bit term sitting inside an o(n) and asked what removing it would cost. That is a size question and the answer is fifteen per cent.

The model’s exposure followed from having something to compare against, and it is the smaller result in bits and the larger one in what it says about the collection’s own instruments. A number reported for several strands, by an object nobody had checked, because there was nothing to check it against.

The generalisable part is about when a model becomes checkable. Before the structure exists, a model’s correctness is a matter of derivation — somebody reads the formula and agrees with it. After the structure exists, it is a matter of measurement, and the two are not the same standard: a derivation checks the formula and a measurement checks the code.

This collection’s habit is measurement over derivation wherever both are available, and a model of an unbuilt structure is the one case where only derivation is. Which means it is the case where the habit is suspended, and the thing to do about that is to notice when it happens rather than to have a policy.

The floor the half-index curve flattens onto, removedWhat dropping the reverse half's locating apparatus saves, against the sampling rate, with the marks held two ways. The published curve is the upper one: it falls by 2.31x across the dial and then flattens at 13.5%, because the marks are n bits whatever the rate and are the whole of what is left to drop. Represent them sparsely and the flat term goes with them: the saving falls by 17.59x, to 1.8% at one in 256. So the celebrated sixth was mostly an artefact of how one array was stored — at one in 4 the two curves are within 0.2% of each other, and by the sparse end they differ by a factor of 7.6.0102030100200one sampled position in every …of both halves saved, per centplain marks: 13.5%Elias-Fano: 1.8%16,384 characters17.6x against 2.3x
Fig. 8 The result the corrected structure produced: a saving that used to flatten at an eighth, falling by seventeen-fold with the flat term properly represented.

The operation the model would have been fine at

There is a use for which the model’s behaviour is not merely acceptable but correct, and it is worth naming because it explains why the class was written the way it was.

A sparse bit vector’s select1(k) — where is the k-th one — is, for an array of positions, pos[k]: one read, constant time, no search. Elias–Fano’s is a select on the high vector plus a low read, which is more work.

So on a workload that is all selects and no ranks, the array of positions is the better structure, and it prices identically. The class is a good implementation of a sparse set for select-heavy use.

The marks are not that workload. A locate asks at at every LF step, which is a rank, and rank1 once at the end — the walk every occurrence at the same price measured. So the operation the marks need most is the one the model is eight times slower at, and the one it is faster at is asked once per occurrence.

That is a specific enough coincidence to be worth stating: the class is right about space, right about the operation nobody needs here, and wrong about the operation the structure is built around.

Which is a reasonable thing to have written, once, for a use that wanted selects. What made it a defect was the name and the reach: a class called after a bit vector, exported from a shared module, priced as an encoding it does not implement, and reached for by four structures across three strands because it was the sparse-set representation available.

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.

Binary searchCheckElias fanoIndex sizeOperation countSample marksSelectSize model