What the machine does

A node costs two ranks

The left child's interval is the position minus the right child's. A descent that calls rank on both children returns exactly the same symbols at twice the cost, and nothing about the answer can see it.

A descent through a wavelet tree carries an interval and splits it at every node. The left child gets the positions whose bit is zero and the right child gets the positions whose bit is one.

Written out, that is four bounds: the left interval is [rank₀(lo), rank₀(hi)) and the right is [rank₁(lo), rank₁(hi)). Four rank queries a node.

It is two, and the reason is one line of arithmetic: rank₀(x) = x − rank₁(x). The zeros before a position are the position minus the ones before it, because every position is one or the other.

So one rank at each end of the interval gives both children, and a node costs two bit-vector ranks rather than four.

The descent enters only the ancestors of the symbols that are thereA wavelet tree over 4,096 characters of a 26-symbol alphabet, with one interval of 64 positions descended. The dark path is what the walk entered: a child is entered only if its half of the interval is non-empty, so the nodes visited are exactly the ancestors of the 24 symbols present. That is 51 nodes and 54 bit-vector ranks against the 130 a loop over the alphabet costs. The pale nodes are the rest of the tree, which the loop pays for and this walk never touches. Each node costs two ranks and not four, because the left child's bounds are the position minus the right child's.acdefghijklmnopqrstuvwyzroot24 of 26 symbols present54 ranks · 51 nodes
Fig. 1 The nodes a descent enters over one interval. Each dark internal node costs exactly two bit-vector ranks; the count is checked against the node count rather than asserted.

Why this is not a micro-optimisation

A factor of two would be worth having anyway, and this one decides whether the operation is worth having at all.

The descent’s whole claim is that it beats a loop over the alphabet. On a twenty-six letter alphabet with four symbols present, the loop costs two hundred and sixty ranks and the descent costs twenty-four. At four ranks a node it would cost forty-eight — still better, and the factor falls from 10.8 to 5.4.

On a wide interval where every symbol is present the loop costs two hundred and sixty and the descent costs fifty-four. At four ranks a node it would cost a hundred and eight, and the factor falls from 4.8 to 2.4.

So the identity is roughly half of the operation’s advantage in the easy case and all of the margin in the hard one. On a two-symbol alphabet, where the loop costs four ranks and the descent costs two, doubling the descent makes the two identical and the operation pointless.

There is a second place the identity pays and it is easy to miss because it is not a rank. The emptiness test — enter a child only if its half of the interval is non-empty — needs both children’s bounds to decide, and having derived one from the other means the test costs a comparison rather than a query. A version computing rank₀ separately would pay the query even for a child it is about to discard.

So the identity and the pruning interact: on a narrow interval most children are empty, and most of the rank₀ queries a four-rank implementation performs are queries about intervals it will not enter. The waste is worst exactly where the operation is meant to be best.

Why no check on the answer can see it

This is the part that makes the identity worth an essay rather than a comment.

A descent that computes rank₀ separately visits the same nodes, enters the same children, reaches the same leaves and reports the same symbols with the same intervals. Every value it returns is correct. The two implementations are indistinguishable by any test of their output, on any input.

What differs is a number that nothing in the answer records.

This collection has a name for that shape and a habit for it: an assertion that has never rejected anything proves nothing, and a property no output can reveal has to be asserted on the operation count. So the check is:

The ranks a descent spends equal twice the internal nodes it entered, exactly.

Not approximately, not within a tolerance. Exactly, because the identity is arithmetic and any deviation is a node doing something other than one rank per end.

Measured on a twenty-six symbol tree over four thousand characters with a two-thousand-position interval: fifty-four ranks for twenty-seven internal nodes. Twenty-six leaves, fifty-three nodes entered, twenty-seven of them internal.

There is a version of this failure that is worse than a doubled cost, and it is the reason the check is an equality rather than an upper bound. An implementation that computes rank₀ instead of deriving it, on a vector whose rank₀ is implemented as n − rank₁ internally, spends the same two queries and charges four — so the counter reports a cost the machine did not pay.

That is the mirror defect: an operation count that overstates rather than an implementation that overspends. An inequality check catches neither and an equality catches both, because both make the count differ from twice the node count.

This collection has met the overstating version before, in a different field. The comparison that is not one comparison is where the unit of a count first had to be pinned down, and the lesson is the same: a counter that is not tied to the structure by an exact relationship is a counter measuring itself.

The rejection test that goes with it

An equality check passes trivially if the thing being counted is wrong in both places, so the strand carries a rejection alongside it.

The rejection removes a different property — the emptiness test — and requires the cost to change while the answer does not. A descent that recurses into every child unconditionally, empty or not, returns exactly the same symbols: an empty subtree contributes nothing, so the output is identical.

Measured on an eight-position interval over a twenty-six symbol alphabet: the pruned walk enters twenty-four nodes and spends thirty-four ranks; the unpruned one enters fifty-five and spends fifty-four. Same seven symbols, same intervals, 1.59 times the cost.

That is the two-ended shape. One check requires the count to be exactly twice the nodes; the other requires that a version doing more work is caught by the count and not by the answer. Together they say that the operation count is measuring the implementation rather than being computed from it.

One cost follows the alphabet and the other follows the answerA 32-symbol alphabet throughout, with the measured stretch drawn from a restricted sub-alphabet so that the number of distinct symbols moves and sigma does not. The loop is flat at 320 ranks: it asks about every symbol whether or not the symbol is there. The descent rises from 10 to 62 — a factor of 32x down to 5.16x — and rises like d log(sigma/d) rather than like d, because the ancestors of few leaves overlap near the root and the ancestors of many do not. At d = sigma the walk enters every node and the two are within 5.16x, which is the two-ended shape of the claim.0100200300102030distinct symbols in the intervalbit-vector ranksthe loop: 320the descentsigma = 32 throughout32x down to 5.16x
Fig. 2 The cost the identity halves, against how many symbols are present. Every point on the lower line is two ranks per internal node entered.
The looser the budget, the more of the search is wastedThe share of extensions that find nothing, against the error budget, for 8-character patterns over 8,192 characters. At zero errors the search follows one path and the dead share is 31.3%; at 2 it is 74.9%, because the intervals a two-error search reaches are narrow and a narrow interval holds few of the alphabet's symbols. The saving follows: 5.75x at no errors and 9.22x at 2. That is the opposite of what a reader might expect from "more work means more to save on" — the work grows and the FRACTION that was never going to help grows with it.02040608000.50011.502errors permittedbranches that find nothing, per cent5.75x6.98x9.22xthe label is thesaving at that budget8 patterns · 8 characters31.3% to 74.9%
Fig. 3 The search this identity keeps affordable: the share of extensions that find nothing, against the error budget, with the saving at each.

Where else the identity applies

The same arithmetic runs through every operation on a wavelet tree and it is worth collecting them, because the saving is the same each time and the reason it is available differs.

A rank walks one root-to-leaf path and needs only one child’s bound at each level — whichever the code says to follow. So it uses one rank a node, not two, and the identity buys it nothing: it never needs the other child.

An access walks down asking which side each position is on, which is one bit read and one rank a level. Same.

A select walks upward and needs the inverse operation, which the identity does not touch: mapping a position in a child back to the parent requires select₀ or select₁ on the parent’s vector, and there is no arithmetic relating those. Select is not rank backwards is where that asymmetry was measured, and it is the reason a tree supporting both operations needs two directories rather than one.

The compound walk — returning a rank and a smaller-count from one descent — uses the identity in a different form. When the code says “go right”, the count of everything that went left is the position minus the ones, which is rank₀ derived rather than computed. The count that was already there is that operation, and its saving is arithmetic on numbers already in hand for the same reason.

The enumeration is the only one that needs both children at every node, so it is the only one where the identity halves the cost rather than being irrelevant.

That is a satisfying pattern: the identity is worth nothing to the operations that follow one path and everything to the one that follows a subtree.

There is a sixth operation worth adding to that list because it is the one this strand’s other half depends on: the document listing descent, which is the enumeration applied to a document array. It uses both children at every node for the same reason and gets the same halving, and the tree answers the question quotes operation counts that assume it.

Two problems, one operation, one identity underneath both. That is worth noticing because it means the identity’s value is not local to a search: any query that needs a subtree of a wavelet tree rather than a path gets it, and any query that needs a path does not.

The classification is that clean. Path operations use one child a node and subtree operations use both, and the identity is worth a factor of two to exactly the second kind.

The general form of the trap

The defect this check exists to catch has a shape that recurs, and it is worth stating without the wavelet tree.

A quantity computed twice when one computation determines the other is invisible to every test of correctness and visible only to a cost measurement. It is not a bug in any sense that a type system, a test suite or a code review of the output would find, and it does not produce a wrong answer under any input.

The reason it survives is that the two computations are usually in different places — one on each branch of a conditional — so the redundancy is not visible in a single line. Here they are rank0(lo), rank0(hi) in one expression and rank1(lo), rank1(hi) in the next, and the relationship between them is a fact about bit vectors rather than about the code.

The way to catch it is the way this collection catches everything of this kind: charge the operations, and assert an exact relationship between the charge and the structure. If the assertion is an inequality it will pass on a doubled cost; if it is an equality it will not.

A wider interval holds more symbols, and then stopsThe two ways of enumerating an interval's symbols, against the interval's width, on 8,192 characters of a 26-symbol alphabet. The loop is flat at 260. The descent starts at 24 ranks for 4 symbols and stops rising at 54, because once the interval holds every symbol there is nothing further to enter — so the saving is bounded below by 4.81x and reaches 11x on the narrow intervals a search actually spends its time in. Both return the same set at every width, which is what makes this a plate about cost.1010010³100positions in the intervalbit-vector ranksd = 4d = 14d = 22d = 26d = 26d = 26the loopthe descentsigma = 2611x narrow, 4.81x wide
Fig. 4 The cost against the interval’s width. The descent’s line is two ranks per internal node throughout, which is what the equality check verifies at every width.

What a rank actually is, and why two is the right unit

The check counts bit-vector ranks, and it is worth being explicit about what one of those costs, because “two ranks a node” is only a meaningful unit if a rank is a meaningful thing.

A rank on a plain bit vector with a two-level directory is: one superblock counter read, one block counter read, and a scan of at most one block’s worth of bits. Three memory touches and a popcount, in the shape rank is the only thing it does measured, and constant in the vector’s length.

On a block-classed vector it is more: the same two directory reads, then a decode of the block from its class and offset before the popcount. Several times the work, which is the price of the compression.

So “two ranks a node” is a fixed number of a variable-cost operation, and the operation’s cost depends on the representation rather than on anything the descent does. That separation is why the count is worth taking: it is a property of the algorithm, and the representation multiplies it by a constant the algorithm has no opinion about.

A count that folded the representation in — “eleven memory touches a node” — would be a count about one representation, and every factor in this strand would have to be re-measured when the vectors changed.

The same symbols, in three different ordersOne interval of 2,048 positions, enumerated on three tree shapes over the same text. All three return the same 21 symbols — the set is a property of the interval and not of the tree. The ORDER is not: a balanced tree and an alphabetic one put their leaves in the alphabet's order, so the symbols come out sorted, and a Huffman tree puts them in frequency order, so they do not. A caller that only wants to know which symbols are present may use any shape. A caller that accumulates "how many characters sort before this one" as it reads the output — which is exactly what a bidirectional extension does — may use only the first two, and reading a Huffman tree's output as sorted produces a plausible number that is wrong.a fixed-length codeleaves in orderacdefghilmnoprstuvwythe best ordered treeleaves in orderacdefghilmnoprstuvwythe best tree of any shapeleaves in frequency ordereahnrstdiloucfgmpvwythe symbols, in the order the descent reports them21 symbols in 2,048 positionsone set, 2 of 3 sorted
Fig. 5 A property of the descent the operation count says nothing about: the order its symbols come out in, which depends on the tree’s shape and not on any arithmetic.

What the identity costs

Nothing, and it is worth saying because the alternative would be a trade.

Deriving rank₀ from the position and rank₁ is a subtraction of two integers already in registers. It requires no extra structure, no extra directory, and no change to the bit vector’s representation. A structure supporting rank₁ supports it.

The one situation where it would not be free is a representation whose rank₀ is cheaper than its rank₁ — a compressed vector storing zero-runs, say — where computing rank₀ directly might beat a subtraction. No representation in this collection has that property, and the check would notice: it requires exactly two ranks a node, so an implementation calling a cheaper rank₀ would fail it and would have to argue its case.

That is the right behaviour for a check. It is not there to forbid a better implementation; it is there to make anyone who writes a different one say so.

What it does to the strand’s numbers

Every factor quoted in this strand assumes the identity, so it is worth stating what the numbers would be without it.

The census factors — 32 at one symbol present, 5.2 at thirty-two — would be 16 and 2.6.

The search factor on a twenty-symbol alphabet with one error — 7.0 — would be 3.5.

The DNA factor of 2.7 would be 1.35, which is close enough to nothing that the operation would be hard to justify on the alphabet the field mostly uses.

So the identity is not an implementation detail of a good operation; it is half the argument for the operation existing. An account of interval_symbols that does not mention it is describing something twice as expensive and with a much narrower range of usefulness.

The saving is a factor in sigma, so a two-symbol alphabet gets twoThe descent against the loop on an interval of 64 positions, across four alphabets. On binary text the loop asks two questions and the descent enters two nodes, so the whole saving is 2.00x and there is nothing to report. On a twenty-six letter alphabet the loop costs 260 ranks against 54 — 4.81x. That ordering matters for what this operation is for: DNA search, which is where most published approximate matching happens, sits near the bottom of this chart at 2.67x, and protein and natural language sit near the top.binary · sigma 22.00x2 against 4dna · sigma 42.67x6 against 16protein · sigma 204.76x42 against 200latin · sigma 264.81x54 against 26064-position interval2.00x to 4.81x
Fig. 6 The alphabets the operation is used on, with the factor at each. Halving the descent’s cost is what keeps the smaller alphabets above one.

Three things the equality would catch

It is worth listing what the check would actually reject, because a check nobody can imagine failing is a check that has not been thought about.

A four-rank descent. The count comes out at four times the internal nodes and the equality fails immediately. This is the defect it was written for.

A descent that visits a node twice. An implementation memoising badly, or recursing on an interval it has already handled, spends more ranks than nodes entered — and the node count is taken from the same walk, so the two would still be in a two-to-one ratio unless the node count is taken independently. It is: the descent returns the nodes it entered and the counter records the ranks, and a doubled visit raises both. So this one is not caught, and saying so is more useful than implying it is.

A counter that charges the wrong thing. If the bit vector’s rank were charging two internally — a superblock read and a block read counted separately — the count would be four times the nodes and the equality would fail. That is the check doing its job on the instrument rather than on the algorithm, which is a use it was not designed for and is the one it has already served: the count is what pins down what a rank means.

The middle case is the honest gap. An equality between a cost and a shape catches a cost that is wrong for the shape, and it does not catch a shape that is wrong. Catching that needs the bound — k(1 + log(σ/k)) ancestors for k leaves — and this strand asserts the cost against the shape rather than the shape against the alphabet.

Most of what a branching search does is find out that nothing is thereAn approximate search for 8-character patterns within 1 error, over 8,192 characters of a 20-symbol alphabet, 8 patterns. The bar is every extension the published shape attempted: 9,206 of 16,800 — 54.8% — produced an empty interval, which is a full rank walk down the wavelet tree whose entire result is the discovery that the character was not there. Enumerating the interval's symbols removes exactly those and keeps the 7,594 live ones, so the search visits the same nodes in the same order and costs 24,086 ranks against 168,000 — 6.98x. The two must find the same occurrences, and on every one of the 8 patterns they do.extensions attempted9,206 dead7,594 livebit-vector ranksthe loop: 168,000the descent: 24,0868 patterns · 1 error · sigma 2054.8% dead · 6.98x
Fig. 7 The measurement everything above is in service of: an approximate search’s extensions, split into the ones that found an interval and the ones that found nothing.

Where the count is quoted

The strand quotes operation counts in three places and they are all this unit, which is worth saying once so a reader can compare them.

Asking about symbols that are not there quotes twenty-four ranks against two hundred and sixty for one interval. Proportional to the answer, not the alphabet quotes ten to sixty-two against a flat three hundred and twenty. The branches that find nothing quotes twenty-four thousand against a hundred and sixty-eight thousand on a search.

All three are bit-vector ranks, all three are two a node on the descent’s side, and all three would double without the identity.

A wider interval holds more symbols, and then stopsThe two ways of enumerating an interval's symbols, against the interval's width, on 8,192 characters of a 20-symbol alphabet. The loop is flat at 200. The descent starts at 18 ranks for 3 symbols and stops rising at 42, because once the interval holds every symbol there is nothing further to enter — so the saving is bounded below by 4.76x and reaches 11x on the narrow intervals a search actually spends its time in. Both return the same set at every width, which is what makes this a plate about cost.1010010³100positions in the intervalbit-vector ranksd = 3d = 12d = 19d = 20d = 20the loopthe descentsigma = 2011x narrow, 4.76x wide
Fig. 8 The same sweep on a twenty-symbol alphabet, where the descent flattens sooner because there are fewer symbols to run out of.

The habit this is an instance of

Three checks in this collection have the same form and it is worth naming the form.

A structural equality between a cost and a shape: ranks equal twice the internal nodes. A cost change with an answer held fixed: the unpruned walk returns the same symbols and costs more. And a two-ended requirement: the operation must be cheap where the claim says and expensive where it does not.

None of the three is a test of the output. All three are tests of the relationship between the output and what was spent producing it, which is the thing an operation count is for and which a correctness test cannot reach.

That is why this collection charges operations at all. A structure that returns the right answer is the easy half; a structure that returns it for the reason its account gives is the half that needs an instrument.

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 vectorCheckDescentInterval symbolsOperation countRankWavelet tree