What the machine does

The count that was already there

A bit vector's rank of one is the position minus its rank of zero. Every walk down a wavelet tree computes one of them at every level, and this collection asked for the other separately for years.

A bit vector supports one operation and everything here is built on it: rank₁(i), the number of ones before position i.

rank₀(i) is not a second operation. It is i − rank₁(i), exactly, always, for arithmetic reasons that no representation can change: every position before i holds either a one or a zero.

One descent, and the count that falls out of itThe walk for "s" in a wavelet tree over 2,048 characters, asked for the first 700 positions. At every level the walk computes one of the two child counts, because that is how it moves to the next level. Where the code says "go right", everything that went left is a smaller symbol — the leaves are in the alphabet's order — and the number of them is the difference between the position and the count already computed. Adding those up gives 543, which is exactly what 21 separate ranks report, and it costs 5 bit-vector operations rather than 105.level 0 · left585 keptlevel 1 · right+351 smallerlevel 2 · right+113 smallerlevel 3 · right+39 smallerlevel 4 · right+40 smallerpositions still in play, and the half the code leaves behindsmaller symbols before position 700: 543rank of "s": 42 — from the same 5 operationsσ 21 · code 011115 ranks, not 105
Fig. 1 One descent, with the two child counts at each level. Only one of them is queried; the other is a subtraction.

That identity is the whole content of the compound operation, and it is why the operation is free rather than cheap.

The measurement

An assertion that something costs nothing has to be measured on a counter rather than read off the code, because “no extra work” is exactly the kind of claim that is true of the version somebody wrote and false of the version that ships.

So: one wavelet tree, one operation counter, a hundred calls of rank and a hundred of rankAndLess at the same positions.

500 bit-vector ranks each. Five per call, which is the depth of a twenty-one-symbol tree. The compound walk performs no operation the plain rank does not.

That check is in the gate, and it fails if the two ever differ — which is the form this collection insists on: the check must reject, and a claim about an operation count is checked by counting operations.

Where the free count comes from

Follow one level. The walk is at position p in a node’s bit vector, and the code says go right.

It needs rank₁(p) to move: that is the position in the right child. One operation.

The count of everything that went left at this level is rank₀(p), which is p − rank₁(p) — a subtraction of two numbers the walk is holding. Nothing is queried.

And if the code says go left instead, the walk needs rank₀(p), and the right-hand count is not wanted at all, because the symbols on the right are larger.

So at every level, one query, and the second child’s count is either free or unwanted. Over the whole descent that is ⌈log₂ σ⌉ operations, which is what a rank costs.

Why a loop was written instead

Two reasons, and both are about how the question was phrased rather than about the structure.

The first: rank_c(i) is an operation the wavelet tree offers, and the smaller-symbol total is not. Faced with an interface that has one method on it, the natural implementation is to call it repeatedly. The tree exposes rank(c, i); the extension needs the sum of rank(d, i) over every symbol d below c; the loop writes itself.

The second: at the alphabet where this structure was first built, the loop is short. On DNA the tree is two levels deep and the loop is at most four terms, so an extension is 18 ranks rather than 6 — a factor of three, on an operation nobody had counted, in a search that was being measured in nodes explored rather than in bit-vector operations.

Two counts disagree is the theme this collection returns to most often, and it applies here in its usual form: the search was being counted in the unit that made it look uniform, and the unit that varies is one level down.

The factor the extension was paying, and what it isBit-vector ranks per interval extension, against the size of the alphabet, for the loop over every smaller symbol and for one compound walk. The loop's line rises with σ because it is σ walks; the compound line rises with log σ, because it is one walk down a tree that deep. At twenty-six symbols that is 139.7 operations against 10.0 — a factor of 14.0, on a search that performs 1,614 extensions. The two structures find the same rows at every point on this plate; only the arithmetic between them changed.1010100symbols in the alphabetranks an extension918110140a rank per smaller symbolone walk8,192 characters · 2 errorsx14.0 at σ 26
Fig. 2 The two units, drawn: bit-vector ranks per extension against the alphabet. The published unit — a symbol rank — is two per extension on both lines.

What “free” is not

Three qualifications, because “costs nothing” invites more than it says.

It is not free of the tree’s shape. The walk’s cost is the code’s length, so a symbol with a long code costs more than one with a short code, on both operations equally. What is free is the second quantity, not the descent.

It is not free of the ordering condition. The subtraction gives the count of everything that went the other way; calling that “smaller” needs the leaves in the alphabet’s order, which costs a few per cent in size and is the subject of the tree the operation insists on.

And it is not free in cache terms. Two ranks at the same level of the same vector touch the same directory entry and probably the same cache line; a loop over σ symbols walks σ different paths and touches σ different lines at every level. So the machine-level saving is larger than the operation-count saving, and this collection does not measure machine-level anything — the cost model is an input, and the model here counts operations.

Where else the identity was available

Once named, the same “the other count is a subtraction” shape turns up in three more places in this collection, and in two of them it was already being used without comment.

Select support. A structure answering select₁(k) — where is the k-th one — is built over the same directories a rank uses, and select is not rank backwards is the essay about why it nonetheless needs its own machinery. What it shares is the arithmetic: a block’s zero count is its width minus its one count, so a directory built for one answers both.

The document array’s complement. In a document index, the rows not belonging to a document are the rows minus the rows that do, which is why nothing here ever stores a complement.

And the interval width in a backward search. ep − sp is the occurrence count, computed from two numbers the search already holds — nobody has ever computed it by counting rows, and nobody would.

The third is so obvious that it makes the first two look strange. All three are the same identity, and only one of them was being used in the place this page is about.

One query, against the length of the text it searchesA pattern of 8 characters looked up in texts from 1,024 to 16,384 characters. The backward search is flat: the same 16 rank queries whatever the text, because it never asks where in the text anything is. The binary search over a suffix array is not, and the gap between them grows. The unit is primitive operations; each structure's own primitive is counted in its own field, and no two of them are the same act. Both axes are logarithmic.10,000100characters of textprimitive operationssuffix array + textcounter array per symbolFM-index, plainFM-index, compressedevery query run with the text withheldEnglish-like
Fig. 3 The operation the whole family reduces to, priced: what a query costs in ranks, which is the unit this page is counting.

What it changes about the earlier numbers

One published measurement moves, and it moves by a factor.

An extension is not one rank: 16.5 ranks an extension on four symbols and 138.6 on twenty-six. That sentence stands as a description of the implementation it described. What it should not be read as is a property of a bidirectional index, because a bidirectional index whose tree has ordered leaves performs 6.0 and 10.0.

The wider claim it supported — that the compound operation is the largest single term in an approximate search’s cost — is confirmed and quantified: on a twenty-six-symbol alphabet the search performs 225,400 bit-vector ranks with the loop and 16,140 with the walk, and every other cost in the search is unchanged.

The general lesson

It is a small one and it is worth stating because it recurs.

When an interface offers one operation and a caller needs a related quantity, ask what the operation’s implementation already computed. A rank down a tree computes a partition at every level, and the partition’s other side was being recomputed by a loop over the whole alphabet.

The reason it survived so long is that both the loop and the walk are correct, both are fast enough at a four-symbol alphabet, and the unit that separated them was one nobody was counting. That is the shape of nearly every constant-factor result in this collection: not a wrong answer, a right answer computed in a unit that hid the cost.

What the operation saves, and what the shape costsBoth currencies on one frame, against the alphabet. The rising line is the factor by which the compound walk cuts the ranks an extension performs — 14.0 at σ 26 — and the flat one along the bottom is what the ordered tree costs in bits, in per cent, on the same texts: never more than 0.17%. On a uniform alphabet the two trees are the same tree and the cost is zero; on real text with a skewed distribution it is a few per cent, which is the number the plate about shapes draws.051015510152025symbols in the alphabetfactor saved, and per cent paidranks saved, as a factorbits paid, per cent8,192 characters · 2 errorsx14.0 for 0.04%
Fig. 4 And the trade this makes available, which the next two pages are about: a factor of fourteen in operations against a few per cent in bits.

What the identity does not give

Three things it might be expected to give and does not, because a free count in one direction is not a free structure.

It does not give select. Knowing how many ones precede a position says nothing directly about where the k-th one is, and select is not rank backwards is the essay about why that needs its own machinery — a sampled directory over the ones, costing bits a rank directory does not.

It does not give the count of a set of symbols that is not a prefix of the alphabet. The walk partitions the alphabet around one symbol, so it answers “how many below c” for free. “How many between c and d” is two walks, and “how many in this arbitrary set” is a walk per symbol again.

And it does not give the distinct symbols present. A search branch would like to know which symbols actually occur in an interval, so that it does not try all σ of them; that is a different traversal — down every node the interval touches rather than down one path — and it costs proportionally to the number of distinct symbols. Named here, not built.

So the free count is exactly one quantity: the number of positions whose symbol sorts before the one being asked about. It happens to be the quantity a bidirectional extension needs, which is why this operation matters at all.

Why the counter is the check

An operation count is the kind of claim that is easy to state and easy to get wrong in a way nobody notices, which is why this collection insists on counting rather than reading.

The failure mode is specific: a helper that looks like arithmetic but is a query. bv.rank0(at) is a method call on a bit vector, and it increments the counter, because it is an operation — a directory lookup and a popcount. Writing at - bv.rank1(at) instead is a subtraction of two numbers in hand. The two lines look almost identical and differ by one bit-vector query per level.

An implementation that used the first form would be correct, would return the same numbers, and would cost exactly twice what this page claims. Nothing about its output would say so.

That is why the check is a counter comparison rather than an output comparison. It fails if the two ever differ, so the sentence “the compound walk performs no operation a plain rank does not” is a thing the build can refuse.

One descent, and the count that falls out of itThe walk for "e" in a wavelet tree over 2,048 characters, asked for the first 300 positions. At every level the walk computes one of the two child counts, because that is how it moves to the next level. Where the code says "go right", everything that went left is a smaller symbol — the leaves are in the alphabet's order — and the number of them is the difference between the position and the count already computed. Adding those up gives 107, which is exactly what 20 separate ranks report, and it costs 5 bit-vector operations rather than 100.level 0 · left253 keptlevel 1 · left158 keptlevel 2 · right+96 smallerlevel 3 · left49 keptlevel 4 · right+11 smallerpositions still in play, and the half the code leaves behindsmaller symbols before position 300: 107rank of "e": 38 — from the same 5 operationsσ 20 · code 001015 ranks, not 100
Fig. 5 The descent on a third text, where the tree’s shape differs and the operation count does not: one query a level, whichever way the code goes.

The saving is per extension, and an approximate search performs thousands of them — 1,614 on a twenty-six-symbol alphabet for a fifteen-character pattern within two substitutions.

At 139.7 bit-vector ranks each that is 225,400 operations, and at 10.0 it is 16,140. Everything else about the search is identical: the same nodes, the same pruning, the same dead branches, the same answers.

So on the alphabet where it matters most, the compound walk removes about 209,000 bit-vector operations from one query. That is the largest single term in the cost of an approximate search on this structure, and it was arithmetic that had been performed and thrown away at every level of every descent since the index strand opened.

The factor the extension was paying, and what it isBit-vector ranks per interval extension, against the size of the alphabet, for the loop over every smaller symbol and for one compound walk. The loop's line rises with σ because it is σ walks; the compound line rises with log σ, because it is one walk down a tree that deep. At twenty-six symbols that is 139.7 operations against 10.0 — a factor of 14.0, on a search that performs 1,614 extensions. The two structures find the same rows at every point on this plate; only the arithmetic between them changed.1010100symbols in the alphabetranks an extension918110140a rank per smaller symbolone walk8,192 characters · 2 errorsx14.0 at σ 26
Fig. 6 The whole result in one line: ranks per extension against the alphabet, for the loop and for the walk.

The habit worth keeping

Two sentences, and they are about interfaces rather than about wavelet trees.

A structure’s operations are the ones somebody exposed, not the ones it can answer. A wavelet tree exposes rank(c, i) because that is what an FM-index needs. The partition of the alphabet around c is computed at every level of every rank and discarded, and it is only visible if somebody asks what the implementation already knows.

And a loop over an alphabet is a signal. Any time a caller iterates over the symbols to accumulate something the structure could accumulate during a single descent, there is a compound operation available. This collection now has two examples — the smaller-symbol count here, and the interval’s distinct symbols, which is named and unbuilt — and the second was found by looking for the same shape.

A balanced wavelet tree over "abracadabra": 217 bitsEvery node is a bit vector: one bit per symbol still in play, saying which half of the remaining alphabet that symbol falls in. A rank for a symbol walks its code from the root, one bit-vector rank a level, and no symbol is ever compared with another. The whole structure is 33 bits of payload and 184 bits of directory and code table, over 11 characters and 5 distinct symbols.00100000010root000101000000101000100001010010one box = one bit · left child 0, right child 15 symbols, 217 bits
Fig. 7 The structure the whole page is about: one bit vector per level, a code word the path to a leaf, and a partition of the alphabet at every step of every descent.

The scope of the identity, as a test with three cases

The closing habit — a loop over the alphabet is a signal — can be sharpened into something a reader can apply without thinking about wavelet trees, because the identity has an exact scope and the scope is easy to state.

A descent partitions the alphabet at every level, and every partition is binary. So one descent determines, for free, the count of symbols below the target and the count above it, and nothing else. Any query that can be written as a count over a prefix of the ordered alphabet is answered by one walk; any query that cannot is not.

That is a small family. There are 2σ2^\sigma subsets of the alphabet and σ+1\sigma + 1 prefixes of it, so the free queries are a vanishing fraction of the possible ones — and the quantity a bidirectional extension needs happens to be among them, which is the whole reason this operation exists rather than a general principle about wavelet trees.

The family is closed under differences, though, and that gives the middle case. A count over an interval [c,d)[c, d) is the prefix count at dd minus the prefix count at cc: two descents, or 2log2σ2\lceil\log_2\sigma\rceil bit-vector ranks. A loop over the same interval costs (dc)log2σ(d-c)\lceil\log_2\sigma\rceil, so the walk wins whenever the interval holds more than two symbols — which is nearly always, and is a saving nobody would notice was available.

So the test has three cases and each has a price.

A prefix of the alphabet — one walk, log2σ\lceil\log_2\sigma\rceil operations, free alongside the rank the caller wanted anyway.

A union of kk intervals2k2k walks, and worth taking over the loop as soon as the intervals hold more than two symbols each.

An arbitrary set, or a property that is not about order at all — the loop is unavoidable, and the right response is a different traversal rather than a cleverer descent. Counting the distinct symbols in an interval is the standing example: distinctness is not a prefix property, no descent computes it, and the operation that does costs O(dlog(σ/d))O(d\log(\sigma/d)) by walking every node the interval touches.

Which explains the three cases this page already lists without a rule between them. The smaller-symbol total is a prefix; the between-cc-and-dd count is an interval; the arbitrary set is neither. The rule is the same one every time: ask whether the quantity is a prefix count in the order the structure sorts by, and if it is, the descent already computed it.

That also says why the identity turns up so often in a bit vector and so rarely elsewhere. A bit vector’s alphabet is two symbols, so every subset of it is a prefix or a complement of one, and the free count is total. A wavelet tree inherits the identity one level at a time and loses most of it, keeping exactly the prefix queries — which is every child at once’s operation and no more. Select is not rank backwards is the boundary from the other side: a query about where rather than how many is not a count over any subset, so no partition answers it and the machinery has to be built.

Where this sits in the strand

Every child at once is the operation and what it saves. This page is why the saving is total rather than partial. The tree the operation insists on is what it costs, and a factor of fourteen, for four per cent puts both on one plate.

The structure underneath all four is the one the index that is smaller than the text builds, and the operation it reduces to is the one rank is the only thing it does is named for.

A last note on units

This collection counts three things where a published account counts one, and this page is a good place to say why all three are kept.

Symbol ranks — what an account of backward search counts. Two per character of the pattern, and it is the right unit for describing what the algorithm does.

Bit-vector ranks — what a wavelet tree performs. The symbol rank times the code’s length, and it is the unit this page’s whole result lives in.

Memory accesses — what a machine performs. A bit-vector rank is a directory read, a word read and a popcount, so it is several, and this collection does not count them because the model here is operations rather than cycles.

Three units, each right for a different question, and the failure mode is always the same: a claim made in one and read in another. The unit of cost is not one is the theme, and it has now produced a factor of five between the first two units on the same operation.

Three shapes, and only two of them can be walkedA wavelet tree over 8,192 characters of english text, built three ways. Huffman's tree is the smallest — it is the smallest tree of any shape, which is what Huffman's construction means — and its leaves are in frequency order, so "everything to the left is smaller" is false in it and the compound walk returns a number with no meaning. The best tree whose leaves ARE in order costs 1.43% more than Huffman's and 20.26% less than a fixed-length code. That is the price of the operation, and it is the whole price.a fixed-length code49,270 bits5.00 ranks · in orderthe best ordered tree39,288 bits3.98 ranks · in orderthe best tree of any shape38,736 bits3.92 ranks · unorderedσ 21 · the ordered tree is 1.43% above the unordered optimum8,192 characters of englishorder costs 1.43%
Fig. 8 Where the second unit’s multiplier comes from: the mean access depth of each tree shape, which is how many bit-vector ranks one symbol rank costs.

Why the check is on the counter and not the clock

One more note about method, because this collection makes the same choice everywhere and it is worth defending once.

The claim is that the compound walk performs no operation a plain rank does not. That could be checked by timing both and finding them equal, and timing would be the wrong instrument: two implementations performing the same operations can differ by a factor of two on wall-clock for reasons about layout and branch prediction, and two performing different numbers of operations can time identically when one of them is memory-bound.

An operation counter answers the question actually being asked. It is exact, it is reproducible on any machine, and it fails loudly if a future edit introduces a query where there was arithmetic.

What it cannot say is whether the saving is felt. That is a different question with a different instrument, and this collection’s cache work is where it lives — two counts disagree exists precisely because an operation count and a machine’s behaviour are two measurements that rank algorithms differently.

Here the two agree in direction and almost certainly in magnitude: one walk down a tree touches one directory entry per level and a loop over σ symbols touches σ of them, so the cache behaviour is better by about the same factor as the operation count. That is an argument rather than a measurement, and it is labelled as one.

What this makes readable

Essays that name this one as a prerequisite.

Named alongside this one

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

What links here

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

The objects this essay names

Each one links to every other essay that touches it.

AlphabetBit vectorCompound operationCounterDirectoryIndex sizeMachineOperation countRankSelectWavelet tree