What the libraries do

A looser budget wastes a larger share

More errors permitted means more work, and the fraction of that work which was never going to help rises with it — from thirty-one per cent at no errors to seventy-five at two. The saving is worth most where the search is most expensive.

An optimisation usually helps least where it is needed most. A cache helps until the working set exceeds it; a filter helps until the selectivity falls; a bound helps until the search space opens up.

This one goes the other way, and the direction is worth measuring because it decides where the operation belongs in a system.

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. 1 The share of a backtracking search’s extensions that find nothing, against the error budget, with the saving at each budget as a label.

The sweep

Eight-character patterns over eight thousand characters of a twenty-symbol alphabet, at zero, one and two errors.

Zero errors. The search follows one path — the pattern’s own characters — so there is one live extension per level and σ − 1 dead ones at each. Dead share 31.3%. Ranks: 6,200 for the loop against 1,078 for the descent, a saving of 5.8.

One error. The search branches once at each level and reaches narrower intervals. Dead share 54.8%. Ranks: 168,000 against 24,086, a saving of 7.0.

Two errors. Dead share 74.9%. Ranks: 2,059,600 against 223,460, a saving of 9.2.

Three things rise together: the absolute work, the dead share, and the saving.

The absolute figures are worth reading beside the shares because they say what regime the search is in. Six thousand two hundred ranks at zero errors, a hundred and sixty-eight thousand at one, two million at two — the work multiplies by twenty-seven and then by twelve. That is the branching factor of an approximate search, and it is the reason this kind of search is expensive at any budget above one on a text of any size.

The search that spends a budget measured that growth on DNA and found the same shape at smaller constants. What this sweep adds is that the composition of the work changes as it grows, and not in the direction that makes it easier to bear.

Why the share rises

A dead extension is one whose character is absent from the current interval, so the dead share is 1 − E[d]/σ where d is how many distinct symbols the interval holds.

An interval of w rows over σ symbols holds about σ(1 − (1 − 1/σ)^w) distinct symbols. At σ = 20 that is 5.2 at w = 6, 12.6 at w = 20, and 19.9 at w = 100.

So the dead share is a function of the interval’s width, and the budget’s effect is entirely through the widths the search reaches.

A larger budget lets the search take mismatching characters, which are rarer than matching ones, so it descends into narrower intervals. It also lets it go deeper before running out — a zero-error search dies as soon as a character is absent, and a two-error search continues past two absences. Both push the width distribution downward.

That is the whole mechanism, and it means the rising dead share is not a property of approximate matching but of interval width. Anything that narrows the intervals a search visits — a longer pattern, a rarer query, a larger alphabet — has the same effect.

One cost follows the alphabet and the other follows the answerA 20-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 200 ranks: it asks about every symbol whether or not the symbol is there. The descent rises from 10 to 42 — a factor of 20x down to 4.76x — 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 4.76x, which is the two-ended shape of the claim.0501001502005101520distinct symbols in the intervalbit-vector ranksthe loop: 200the descentsigma = 20 throughout20x down to 4.76x
Fig. 2 The relationship the budget acts through: how the descent’s cost follows the number of symbols present, at a fixed alphabet of twenty.

The coupon-collector expression also says how sharp the transition is, and it is sharper than a reader might expect. On twenty symbols the interval width at which half the alphabet is present is about fourteen rows; at six rows a quarter is present and at forty rows nearly all of it. So the dead share goes from 74% to 5% over a width range of six to forty.

A search’s intervals span that whole range in a single query — wide at the top, narrow at the bottom — so the aggregate dead share is a weighted average over a distribution that is bimodal in effect. That is another reason a single number for “the dead share” is a summary of something with structure, and it is why the plate reports it at three budgets rather than once.

What that means for where to apply it

An optimisation whose value rises with the problem’s difficulty belongs in the hard path rather than in the fast path, and this one is the reverse of the usual advice.

A system doing mostly exact lookups with occasional fuzzy fallback should put the enumeration in the fallback. The exact path gets nothing from it — the saving that is a loss measures the enumeration costing about twice the walk on a search that knows its character — and the fallback gets between five and nine.

That is a rare shape and it is worth naming: an optimisation that is a pessimisation on the common case and a large win on the expensive one. The correct deployment is a branch on the query type rather than a global replacement, which is more code than a global replacement and is what the numbers say.

Most of what a branching search does is find out that nothing is thereAn approximate search for 8-character patterns within 2 error, over 8,192 characters of a 20-symbol alphabet, 8 patterns. The bar is every extension the published shape attempted: 154,266 of 205,960 — 74.9% — 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 51,694 live ones, so the search visits the same nodes in the same order and costs 223,460 ranks against 2,059,600 — 9.22x. The two must find the same occurrences, and on every one of the 8 patterns they do.extensions attempted154,266 dead51,694 livebit-vector ranksthe loop: 2,059,600the descent: 223,4608 patterns · 2 error · sigma 2074.9% dead · 9.22x
Fig. 3 The two-error search, where three quarters of every extension attempted produces an empty interval.

The absolute numbers, which are the reason

The shares are the mechanism and the absolute counts are why anyone cares.

At two errors the published search spends two million fifty-nine thousand six hundred bit-vector ranks for twelve patterns. That is a hundred and seventy thousand ranks a pattern, on a text of eight thousand characters.

The enumeration spends two hundred and twenty-three thousand — eighteen thousand a pattern.

A search costing a hundred and seventy thousand operations on eight thousand characters is a search that does not scale to a real text without something changing, and this changes one factor of nine of it. That is not enough on its own — the field’s answer to a two-error search on a large text is a partitioning scheme rather than a faster inner loop — and it is a factor that composes with those.

The search that starts in the middle is where the partitioning schemes were measured here, and the two are independent: a scheme decides which searches to run and the enumeration decides what each costs.

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. 4 The width axis the budget acts through, drawn directly: the two costs against the interval’s width, on the same alphabet.

Where the budget stops mattering

The sweep stops at two errors and it is worth saying what happens past it, because the trend cannot continue indefinitely.

As the budget grows the search eventually visits every interval in the index, at which point the dead share is the average over all intervals rather than over narrow ones — and the average over all intervals includes the root, where nothing is dead.

So the dead share rises, peaks, and falls back toward the census figure as the budget approaches the pattern length. At a budget equal to the pattern length every string of that length matches and the search is a full traversal.

That is a degenerate regime nobody runs and it bounds the trend. Between zero and about a third of the pattern length — which is where real approximate matching lives — the share rises monotonically, and the three points measured are all in that range.

The pattern length, which moves with it

The budget is not the only dial and the two interact, which the sweep above holds fixed at eight characters.

A longer pattern at a fixed budget reaches narrower intervals, because more characters have been matched before the search gets deep. So a twelve-character search at one error has a higher dead share than an eight-character one.

A shorter pattern at a fixed budget is looser in relative terms — one error in four characters is a lot of freedom — so it stays in wide intervals and has a lower share.

What actually determines the regime is roughly the ratio of budget to length, and the three points here are 0, 1/8 and 1/4. A workload at 2/12 sits between the second and third.

That is worth stating because a factor quoted at “one error” is not comparable across pattern lengths, and read-alignment workloads use patterns of a hundred characters with budgets of five — a ratio of 1/20, which is tighter than any point here.

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 9.5%; at 2 it is 28.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: 2.75x at no errors and 2.94x 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.010203000.50011.502errors permittedbranches that find nothing, per cent2.75x2.83x2.94xthe label is thesaving at that budget8 patterns · 8 characters9.5% to 28.9%
Fig. 5 The same sweep on a four-symbol alphabet, where the dead share is far lower at every budget because a narrow interval still holds most of four symbols.

The other quantity that rises

There is a third thing rising with the budget and it is the one that eventually decides whether any search is affordable: the number of occurrences reported.

A zero-error search reports the pattern’s exact matches. A one-error search reports those plus everything within one edit, which on a text of eight thousand characters and a twenty-symbol alphabet is a much larger set. A two-error search reports a set larger again.

That matters because the enumeration removes work from the search and not from the reporting. Once the intervals are found, turning their rows into text positions costs LF steps proportional to the sampling gap, per occurrence — and a search whose occurrence count is multiplying by ten per error will spend more on locating than on searching at some budget.

So the factor of nine at two errors is a factor of nine on a component whose share of the total is falling. That does not make it worthless; it makes it a factor on the part of the query that the budget is inflating fastest in operations, while a different part inflates fastest in output.

The composition is not measured here and the two parts are measured separately elsewhere: every occurrence at the same price is where locating’s cost per occurrence was pinned down. Putting them together for a specific budget and sampling rate is arithmetic a system can do and this collection has not.

On DNA, which is where the work is

Repeating the sweep on four symbols gives a much flatter picture: 9.5% dead at no errors, 18.9% at one, 28.9% at two. Savings of 2.7, 2.8 and 2.9.

The share still rises and the saving barely does, because the saving’s ceiling on four symbols is log₂ 4 = 2 at the worst case and 4 at the best, and there is nowhere to go.

So the encouraging shape of this essay — the operation is worth most where the search is most expensive — is a property of large alphabets. On DNA the operation is worth about the same everywhere, which is 2.8.

Two at binary, five at twenty-six makes that argument in full. The point here is narrower: the budget dependence is also an alphabet effect, and a system tuning its fallback threshold on protein numbers would be tuning on the wrong curve.

Three other query shapes produce the same effect and are worth naming, because a system may meet them without ever calling anything an error budget.

A wildcard. A pattern with a dot in it branches over every symbol at that position, which is exactly one level of full branching. A pattern with three dots is a three-level branch, and the intervals below it are as narrow as a three-error search reaches.

A character class. A pattern with [aeiou] branches over five characters rather than one, and the fifteen absent ones are dead extensions in the loop.

A regular expression over an index. Every alternation and every closure is a branch, and the search visits an interval per state of an automaton — which is a large amount of branching at every position.

All three have the same shape as an error budget: they multiply the number of extensions attempted and narrow the intervals reached, so the dead share rises and the enumeration’s value rises with it.

The last is the interesting one because the branching there is not bounded by a small integer. An error budget of two is two levels of branching; a regular expression can branch at every position of an arbitrarily long match. So the regime this sweep’s right-hand end approaches is the ordinary regime for an index walked by an automaton, and the operation’s value there should be at the top of the range measured here or above it.

That is a prediction rather than a measurement — nothing in this collection walks an index with an automaton — and it is the kind of prediction the sweep’s shape licenses.

The descent enters only the ancestors of the symbols that are thereA wavelet tree over 4,096 characters of a 20-symbol alphabet, with one interval of 16 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 12 symbols present. That is 32 nodes and 40 bit-vector ranks against the 100 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.efhiknprstvyroot12 of 20 symbols present40 ranks · 32 nodes
Fig. 6 A narrow interval on twenty symbols, with the nodes a descent enters: the regime a loose search spends most of its time in.

What a threshold would look like

Putting the pieces together, a system deciding when to use the enumeration has a threshold and the threshold has three arguments.

The alphabet, which sets the range. Below about eight symbols there is nothing to decide.

The budget-to-length ratio, which sets where in the range the search sits. Below about 1/16 the search is nearly exact and the enumeration is closer to a loss.

Whether the search branches at all. Which is the only one of the three that is a hard switch rather than a gradient.

The third dominates and the first two are refinements. A system that uses the enumeration for every branching search and the walk for every exact one has captured almost all of the available benefit, and tuning the other two buys a few per cent.

That is a comfortable place to end a tuning discussion and it is worth saying why it is comfortable: the switch is on a property of the query — does it branch — which is known before the search starts and does not need measuring. That is the same property the threshold that reaches zero found made a filter’s threshold usable: a decision that can be taken from the query rather than from the data is a decision that does not have to be tuned.

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. 7 The parameter that decides whether the budget sweep is interesting: the alphabet, across which the operation’s saving spans a factor of two.

Two ways to spend a budget, and only one of them helps here

An error budget can be spent in two places and the sweep above spends it in one, which is worth flagging because the other behaves differently.

Spent on the pattern, which is what a backtracking search does: at each position, try the pattern’s character for free and every other character at a cost of one. That is the search measured here.

Spent on a partition, which is what a filtering scheme does: cut the pattern into k + 1 pieces, observe that one piece must match exactly, search for each piece exactly and verify the candidates. The q-grams an error cannot destroy is that argument and the search that starts in the middle is the version that runs inside an index.

The enumeration is worth a great deal to the first and almost nothing to the second, because a partitioned search’s pieces are searched exactly: it knows its characters and never branches. So a system that has adopted a partitioning scheme has, incidentally, moved itself out of the regime where this operation pays.

That is not an argument against partitioning — the schemes are worth far more than a factor of nine — and it is an argument for measuring which regime a system is in before quoting either. A search doing both, which is what a real aligner does, has an exact phase where the enumeration is a small loss and a verification phase where it may be a large win.

Most of what a branching search does is find out that nothing is thereAn approximate search for 12-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: 8,564 of 16,180 — 52.9% — 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,616 live ones, so the search visits the same nodes in the same order and costs 23,414 ranks against 161,800 — 6.91x. The two must find the same occurrences, and on every one of the 8 patterns they do.extensions attempted8,564 dead7,616 livebit-vector ranksthe loop: 161,800the descent: 23,4148 patterns · 1 error · sigma 2052.9% dead · 6.91x
Fig. 8 The same alphabet at a longer pattern, where the intervals reached are narrower and the dead share higher again.

The general shape

Two things in this essay generalise past the operation.

An optimisation’s value can be measured against the difficulty of the problem, and the sign is worth knowing. Most help less as the problem hardens. This one helps more, and the reason is structural — it removes work proportional to the search’s own branching, so a search that branches more has more removed.

A share that rises with a dial is not the same as a saving that rises with it. The dead share rises from 31% to 75% — a factor of 2.4 — and the saving rises from 5.8 to 9.2, a factor of 1.6. The gap is that removing three quarters of the work is a factor of four and the descent’s own cost is not zero.

The branches that find nothing is where the dead share is counted directly and where the factor of seven at one error is decomposed into its two halves. A reader given only the shares would over-predict the savings by half. That is a small instance of a general habit this collection has: report the quantity the mechanism produces and the quantity a caller pays, because the relationship between them has a constant in it.

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.

Alphabet sizeApproximate matchingBacktracking searchDead branchDescentError budgetInterval symbols