What the libraries do

The saving that is a loss

An operation that is seventy-eight times cheaper on a branching search costs twice as much on an exact one. It reports every symbol present in order to hand back the one that was asked for, and a search that knows its character needs none of the rest.

The interval enumeration is the largest operation saving in this strand: 78× on a backtracking search over a twenty-symbol alphabet at one error.

On an exact search it costs 1.90 times what it replaces.

On an exact search the enumeration is the more expensive of the twoThe same three machines on a search that knows which character it wants at every step: 16 exact patterns of 12 characters, in bit-vector ranks per step. The loop costs 107.9, the compound walk 10.0 — 11x, and one walk per step is all an exact search needs. The descent costs 19.0, which is 1.90x the walk, because it reports every symbol present in order to hand back one of them. So the two operation savings are a choice rather than a stack, and what chooses is the shape of the search: a branch wants the descent and a known character wants the walk.a loop over the alphabet107.9the compound walk10.0the cheapest hereone descent per step19.016 exact patterns of 12ranks per step · sigma 21
Fig. 1 Three machines on a search that knows which character it wants at every step: sixteen exact patterns of twelve characters, in bit-vector ranks per step.

The measurement

Sixteen exact patterns of twelve characters, over eight thousand characters of a twenty-symbol alphabet.

A loop over the alphabet: 107.9 ranks a step. It asks about every symbol and uses one.

The compound walk: 10.0 ranks a step. One root-to-leaf descent for the named symbol, returning its rank and its smaller-count.

The enumeration: 19.0 ranks a step. It descends the subtree containing every present symbol, collects each one’s interval, and the search picks out the one it wanted.

So the enumeration is 5.7× better than the loop and 1.90× worse than the walk.

Why

The two operations answer different questions and an exact search only asks one of them.

The walk answers “where does symbol c go?” — one path, 2⌈log₂ σ⌉ ranks, which at σ = 20 is ten.

The enumeration answers “which symbols are here, and where does each go?” — one subtree, 2d(1 + log₂(σ/d)) ranks for d symbols present.

At d = 1 those are the same descent. At d = 20 the enumeration costs about twice, and returns twenty times as much.

An exact search wants one answer per step. Getting nineteen more is not free, and the enumeration’s cost is the price of the nineteen nobody asked for.

There is a second way to see the same thing that makes the magnitude predictable rather than measured.

The enumeration’s cost at d = σ is 2(2σ − 1) ranks, or about 4σ. The walk’s is 2⌈log₂ σ⌉. At σ = 20 those are 78 and 10, a ratio of 7.8 — which is nowhere near the measured 1.90.

The measured figure is smaller because an exact search does not sit at d = σ. Its intervals narrow as the pattern is consumed, so the number of symbols present falls: at the root all twenty, and six characters in perhaps three or four. Averaged over the twelve steps of a twelve-character pattern, the enumeration’s cost is 19.0 rather than 78.

So the loss is worst at the start of an exact search and nearly nothing at its end, which is the opposite of where a backtracking search’s saving is worst. Both are consequences of intervals narrowing, read in opposite directions.

The crossing is not on a dial

It would be convenient if the two operations crossed somewhere on a parameter a system could tune, and they do not.

The relevant quantity is how many symbols the search wants at this node. An exact search wants one, always, at every node. A backtracking search wants all of them, at every node.

There is nothing between. A search either has a specific next character or it does not, and the answer is a property of the query rather than a setting.

So the “crossing” is a switch, and the switch is on something known before the search starts.

That is a comfortable place for a decision to be. This collection has several thresholds that need measuring against data — a filter’s selectivity, a cache’s size, a sampling rate — and this one is decided by the query’s own shape.

On an exact search the enumeration is the more expensive of the twoThe same three machines on a search that knows which character it wants at every step: 16 exact patterns of 12 characters, in bit-vector ranks per step. The loop costs 136.2, the compound walk 10.0 — 14x, and one walk per step is all an exact search needs. The descent costs 20.6, which is 2.06x the walk, because it reports every symbol present in order to hand back one of them. So the two operation savings are a choice rather than a stack, and what chooses is the shape of the search: a branch wants the descent and a known character wants the walk.a loop over the alphabet136.2the compound walk10.0the cheapest hereone descent per step20.616 exact patterns of 12ranks per step · sigma 27
Fig. 2 The same measurement on a twenty-six letter alphabet, where the loop is worse, the walk is unchanged in shape, and the enumeration’s loss is larger.

The loss grows with the alphabet, which is the direction that makes the branch worth having rather than optional. On four symbols the enumeration on an exact search costs about 1.3 times the walk; on twenty-six it is over two.

The reason is that an exact search’s early intervals hold more symbols on a larger alphabet, so the enumeration returns more that nobody asked for. The walk’s cost rises only logarithmically.

So on the alphabets where the enumeration is most valuable for branching searches, it is also most wasteful for exact ones, and a system on a large alphabet has the most to gain from choosing per node.

What a real search does with both

The consequence is that an index should support both operations and choose per node, which is more machinery than choosing one and is what the numbers say.

The index this strand builds has both: extendLeft for a named symbol, using the compound walk, and branchLeft for every present symbol, using the enumeration. They share every array — the same wavelet tree, the same C table, the same directories — so having both costs nothing in bits.

A search calls whichever suits its node. An exact backward search calls the first m times. A backtracking search calls the second at every node. A partitioned search — the pigeonhole schemes, where a pattern is cut into pieces and each piece is searched exactly — calls the first inside its pieces and the second only if it verifies approximately.

That third case is the interesting one because it is what a real aligner does, and it means a production search spends most of its time in the regime where the enumeration is a loss.

Three ways to extend an interval, on the same searchA search for 8-character patterns within 1 error over 8,192 characters of a 21-symbol alphabet, 6 patterns, counted in bit-vector ranks. The published shape asks about every character of the alphabet at every node: 1,478,400. The compound walk makes each of those questions cheaper by returning the rank and the smaller-count from one descent, which is 11x. Enumerating the interval's symbols removes the questions instead, which is 78x — and the 55.8% of extensions that found nothing are what it removed. All three visit the same live intervals in the same order and report the same occurrences; that is the check, and it is what makes this a plate about cost.a loop over the alphabet1,478,400the compound walk, inside the loop134,40011xone descent per node18,91678x6 patterns · 1 error · sigma 2155.8% of the extensions were dead
Fig. 3 The other regime, for comparison: the same three machines on a search that branches at every node.

What a loop was doing there in the first place

Both the walk and the enumeration are replacements for a loop over the alphabet, and it is worth asking why the loop was ever the shape, because the answer explains why two replacements exist.

A bidirectional extension needs two things: the interval for the new symbol, and the count of symbols sorting before it. The second is where the loop came from — the published formulation computes it by iterating over the alphabet below the symbol and summing interval widths, which is σ ranks in the worst case.

So the loop is not a way of finding which symbols are present; it is a way of computing a sum. That the same loop also discovers absence is incidental.

The compound walk replaces the sum with an arithmetic identity along one descent. The enumeration replaces the whole loop with a subtree walk that produces the sum as a running total.

Two replacements for one loop, aimed at two different things the loop was doing, which is exactly the condition under which one subsumes the other. Two factors that do not multiply is the arithmetic of that, and this essay is the boundary of when each is the right replacement.

Three ways to extend an interval, on the same searchA search for 8-character patterns within 1 error over 8,192 characters of a 27-symbol alphabet, 6 patterns, counted in bit-vector ranks. The published shape asks about every character of the alphabet at every node: 2,638,440. The compound walk makes each of those questions cheaper by returning the rank and the smaller-count from one descent, which is 14x. Enumerating the interval's symbols removes the questions instead, which is 109x — and the 60.5% of extensions that found nothing are what it removed. All three visit the same live intervals in the same order and report the same occurrences; that is the check, and it is what makes this a plate about cost.a loop over the alphabet2,638,440the compound walk, inside the loop188,46014xone descent per node24,100109x6 patterns · 1 error · sigma 2760.5% of the extensions were dead
Fig. 4 The three machines on a twenty-six letter alphabet, where the loop’s cost is largest and both replacements are worth most.

What the numbers are worth in each regime

Putting the two side by side, because the asymmetry in magnitude matters as much as the sign.

On a branching search, the enumeration is 78× better than the loop and 7.1× better than the walk. Large.

On an exact search, the walk is 10.8× better than the loop and 1.90× better than the enumeration. The first factor is large and the second is small.

So the cost of choosing wrongly is asymmetric: using the walk on a branching search costs a factor of seven, and using the enumeration on an exact search costs a factor of two.

A system forced to pick one should pick the enumeration, and would give up a factor of two on the queries it probably runs most.

A system that can pick per node gives up nothing, and the machinery for it is two methods rather than one.

The asymmetry has a second consequence for how the two should be described. A factor of seventy-eight quoted for the enumeration invites reading it as strictly better, and the correction — that it is 1.90 times worse in the other regime — is small enough to be dismissed as a detail. It is not a detail; it is the whole reason the walk stays in the index.

The size ladder, and the last rung spends what the others savedA bidirectional index over 16,384 characters at one sampled position in 32, with each change applied on top of the last. Dropping the reverse half's locating apparatus takes it to 90.4%. Representing the remaining marks as an Elias-Fano array takes it to 84.2% — and that second step is worth 15,570 bits against the first step's 24,080, which is most of a saving that was attributed entirely to the first. The last rung is not a saving at all: it spends the whole of what was saved on sampling the surviving half four times as densely, and lands at 96.4% of where it started with a locate several times faster. That is the trade the strand exists for, and it is one plate rather than two.both halves locate250,584100.0%the reverse half counts only226,50490.4%and its marks are Elias-Fano210,93484.2%the saving spent on sampling241,47596.4%16,384 characters · one in 3284.2% smaller, or the same size and faster
Fig. 5 What the two operations do not affect: the structure’s size, which is decided by a separate set of changes and composes with either.

It is worth being explicit that having both operations costs no bits, because that is what makes the branch free rather than a trade.

Both are methods on one wavelet tree. The walk descends one path; the enumeration descends a subtree. Neither adds an array, a directory or a table, and an index supporting rank supports both.

So the machinery cost of the branch is a conditional in the search’s inner loop and two methods where there was one. That is the whole of it, and it buys a factor of two on exact queries and seven on branching ones against picking either alone.

Why the loss is only two rather than twenty

The enumeration returns twenty times as much information and costs twice as much, which is worth explaining because it is the reason the loss is bearable.

A descent visiting d leaves enters d(1 + log₂(σ/d)) nodes, and the ancestors of many leaves overlap near the root. At d = σ that is 2σ − 1 nodes for σ leaves — under two nodes a leaf.

A rank walk for one symbol enters log₂ σ nodes for one leaf — five nodes a leaf at σ = 20.

So the enumeration is more efficient per symbol by a factor of two and a half, and it is less efficient overall because it produces twenty symbols when one was wanted.

That is the general shape of a batch operation. It is cheaper per unit and more expensive in total when the batch is larger than the need, and the crossing is at a batch size of one — which is exactly where an exact search sits.

The check that stops the headline travelling

This strand’s rejection test for the enumeration is not about a wrong answer; it is about a claim, and it is worth describing because a check on a claim is an unusual object.

The test runs an exact search on both machines and requires the enumeration to cost more. If it did not, the boundary this essay is about would not exist and the headline factor could be quoted without a condition.

A test that requires a structure to be worse reads backwards, and it is the right shape: what is being asserted is that a boundary exists where the argument says, and a claim about a boundary needs a point on each side.

The same shape appears three times in this slate — a sparse representation required to lose when dense, a compressed array required to save nothing on an even collection, and this — and each time the check is on the unfavourable case. That is the half a measurement made by somebody hoping for a result will not make.

Where the sparse representation loses is the clearest of the three, and its argument applies unchanged: a check that only ever runs on inputs where the claim holds has been fitted by its inputs, and an exact inequality with no tolerance in it can still be a check that cannot fail.

What this does to the strand’s headline

The strand’s largest number is 78×, and this essay is the boundary of the claim.

Stated fully: on a search that branches, over an alphabet of twenty symbols, at one error, the enumeration costs a seventy-eighth of a loop over the alphabet. Change any of the three conditions and the number moves; remove the first and the sign changes.

Two at binary, five at twenty-six moves the alphabet: on DNA the branching factor is 2.8 rather than 78.

A looser budget wastes a larger share moves the budget: 5.8 at no errors on protein, 9.2 at two.

And this moves the query shape, which is the condition that flips the sign rather than scaling the number.

Three conditions, three essays, and the one that flips the sign is the one a reader is least likely to check — because “does this search branch?” sounds like a question about implementation rather than about applicability.

One saving is flat in the budget and the other is notThe two savings against the error budget, on 8-character patterns over 8,192 characters of a protein alphabet. The compound walk is flat at 11x — it makes each extension cheaper by a factor that is a property of the alphabet, and the budget does not change the alphabet. The enumeration moves with the budget, because what it removes is the extensions that find nothing and the share of those rises from 64.5% at no errors to 74.7% at 2. The two lines never cross here, and that is the finding rather than a limitation: on a branching search the descent wins at every budget, and the crossing is on the other axis entirely — whether the search branches at all.025507510000.50011.502errors permittedfactor against the published loop64.5%55.8%74.7%the walk: 11xthe descentthe label is the shareof dead extensions6 patterns · sigma 2198x to 105x
Fig. 6 The two savings against the error budget on a branching search, where neither line crosses the other and the crossing is on a different axis.

What a partitioned search actually spends

The claim that a production search spends most of its time in the exact regime deserves numbers rather than an assertion, and this collection has the pieces even though it has not put them together.

A pigeonhole scheme cuts a pattern of m characters into k + 1 pieces, observes that one piece must match exactly if there are at most k errors, searches each piece exactly, and verifies the candidates. The q-grams an error cannot destroy is the filtering argument and the search that starts in the middle is the version that runs inside a bidirectional index.

The exact searches are k + 1 searches of m/(k+1) characters each — so m steps in total, every one of them a named-symbol extension.

The verification is either a direct comparison against the text, which uses no index operations at all, or a bounded approximate search around each candidate, which branches.

So the split between the two regimes is the split between filtering and verification, and which dominates is the filter that feeds the table’s subject: a filter proposing many candidates spends its time verifying and one proposing few spends it filtering.

That is a measurement this strand does not make and it is the one that would say what an aligner’s mix of the two operations actually is. What can be said without it is that both regimes occur in every query, which is the argument for the branch.

Bits paid and steps spent, against the one dial that moves bothThe whole structure's size as a share of the plain bidirectional index, and the LF steps one occurrence costs, on the same frame and against the same parameter. The size falls from 78.0% to 84.7% as the sampling thins, and the walk to a sampled position rises from 1.5 steps to 67.5. Neither line is the answer on its own: a reader choosing a sampling rate is choosing a point on this pair, and a plate showing only the first reports a structure that gets better forever. The two operation savings this strand measures move neither line, which is what "orthogonal" means here and is why they compose with this and not with each other.020406080255075100125one sampled position in every …of the plain index, per centsize: 84.7%67.5 steps16,384 characters · 8-character patternssize falls, the walk lengthens
Fig. 7 A dial that affects neither operation: the structure’s size and a locate’s steps against the sampling rate, which the search’s shape does not touch.

Three regimes, one index

Collecting what the index built here does, because the answer is a small table rather than a rule.

An exact backward search. m named-symbol extensions, using the compound walk. Ten ranks a step at σ = 20. The enumeration would cost nineteen.

A backtracking search. Every node branches, using the enumeration. Eighteen thousand nine hundred ranks for twelve eight-character patterns at one error. The walk inside a loop would cost a hundred and thirty-four thousand.

A locate. Neither operation. s/2 LF steps, each a rank and an access on the wavelet tree, plus a mark lookup — which is where a position split in two’s representation is read and where the strand’s third saving lives.

Three phases of a query, three different parts of the structure exercised, and the two operation savings apply to one phase each while the size saving applies to the third.

That is the tidiest statement of what three savings on one structure found: not three improvements to one thing, but three improvements to three phases, two of which had been described as improvements to the same one.

Two savings that do not multiply, and the amount by which they do notThe compound walk is 11x and the enumeration is 78x against the same baseline, so a reader with both numbers computes 860x for an index that has both. The measured figure is 78x, and the shortfall is 11x — which is the compound walk's own factor, exactly. It is not an interaction, an overhead or a rounding: one descent over an interval returns each present symbol's sub-interval, which IS the walk's rank, and running-summing the counts gives the walk's smaller-count for every symbol at once. Taking both is taking the second one twice.factor against the published loopthe compound walk11xthe enumeration78xthe two, multiplied860xnothing measures thisan index with both78xthe shortfall is 11x, and the walk's factor is 11x6 patterns · 1 error · sigma 21the larger of the two, not the product
Fig. 8 The arithmetic that follows from the two operation savings being about one phase each: a product a reader would compute, against what an index with both actually costs.

The general shape

A saving that is a loss in a neighbouring regime is a common enough object to be worth a name, and the useful question about one is where the boundary is.

Here the boundary is sharp and known in advance: one symbol wanted or all of them. That is the best case.

The uncomfortable case is a boundary that is neither sharp nor known — a saving that helps on data of one shape and hurts on another, where the shape has to be measured and can change. This collection has several: a filter’s threshold, a cache’s working set, a sampling rate.

The distinction is worth drawing because it decides whether an adaptive implementation is needed. A boundary known from the query is a branch; one known from the data is a measurement; one that moves is a controller.

This is a branch, which is why the answer is to have both operations and call the right one.

There is one further property that makes it the best kind of branch, and it is worth naming because it is not automatic even for a query-determined decision. The branch is on a static property of the call site rather than on a run-time value: an exact search’s code always wants one symbol and a backtracking search’s always wants all of them, so the choice is made when the search is written rather than when it runs.

That means there is no predicate to evaluate, no misprediction, and no code path that has to handle both. Two searches, two methods, and neither ever calls the other’s.

A decision that can be moved from run time to write time is worth moving, and the way to find such decisions is the question this essay asks: not “which is faster” but “what does the caller know”. An exact search knows its character; a backtracking search knows it does not have one; and the operation each should use follows from what each knows rather than from either’s cost.

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.

Backtracking searchBackward searchBidirectional indexCompound walkInterval symbolsRankWavelet tree