What the machine does

What a character costs on four machines

Forty-four operations, one, forty-two, and a number that moves. Four machines for one language, with the construction charged separately from the steps, because a machine that is free per character paid eleven thousand operations before the first one.

One expression, one text of four thousand characters, four machines.

What a character costs, and what was paid before the first oneThe four machines on 4,096 characters against (a|b)*a(a|b)(a|b)(a|b)(a|b)(…, with the construction charged separately from the steps. The NFA costs 43.7 operations a character, which is the size of the state set it is advancing. The DFA costs exactly 1 — one table lookup — and paid 11,297 operations to build 128 states first. The bit-parallel simulation costs 37.2, which is the same state set advanced in 1 machine words: a constant factor on the NFA rather than a change of class, because Thompson's splits make the epsilon closure an iterated or over the whole word rather than a shift.Thompson's NFA, a set of states43.724 to buildthe subset DFA, built in full1.011,297 to buildthe subset DFA, built on demand41.824 to buildthe state set in machine words37.224 to buildoperations per character; the note is what was paid once, before the first character4,096 characters · k = 643.7 down to 1
Fig. 1 Four machines on the same text and the same expression, with what each paid before the first character charged separately from what each pays per character.

Thompson’s NFA, carrying a set of states: 43.7 operations a character, 24 to build.

The subset DFA, one table lookup: 1.0 a character, 11,297 to build.

The lazy DFA with a cache of sixty-four: 41.8 a character, and 45 cache flushes.

The bit-parallel simulation, one machine word: 37.2 a character, 24 to build.

The two currencies

A matcher’s cost has two parts and reporting only one of them makes a machine look free.

The construction is paid once per expression. The DFA’s 11,297 operations are 64 states each requiring an NFA step per alphabet symbol, and each NFA step is a state-set walk with closures.

The steps are paid once per character. The DFA’s are one table lookup, exactly, at every text length.

So the DFA is a hundred and seventy times more expensive to build than the NFA and forty-four times cheaper to run, and which is better is a question about the text’s length. Where the table starts paying is the crossing.

The exponential is in the expression, and only in some expressionsStates against k, for two families. The upper line is the subset DFA of (a|b)*a(a|b)(a|b)(a|b)(a… — 512 states at k = 8, which is 2^(k+1) exactly and not approximately: the machine has to remember which of the last k characters were an a, and every one of those 2^k answers is a distinct set of NFA states. The NFA for the same expression has 30 states and grows by two per operator. The lower line is a literal of the same length, whose DFA has 11 states — one per character. Both families are "a regular expression"; the difference is that one of them asks the machine to remember something and the other does not.110100k, the operators after the alternationstatesits DFA: 512its NFA: 30a literal's DFA: 11alphabet ab2^(k+1), exactly
Fig. 2 Where the DFA’s construction cost comes from: the number of states the subset construction discovers, which doubles with the expression on this family.

That the two currencies have to be separated is not an accounting nicety; it is what makes the four machines comparable at all. Three of them have a construction cost of about twenty-four operations and one has eleven thousand, and a total that mixed the two would report the DFA as the most expensive machine on any text under eleven thousand characters — which is true and is not what a per-character comparison is asking.

The convention this settles: a cost paid once and a cost paid per unit are separate columns, and a plate showing one says which. This collection has applied it to construction against query throughout, and the regular-expression machines are the first case where the two differ by three orders of magnitude.

Why the DFA’s step is exactly one

A deterministic machine’s state is an integer and its transition is table[state][symbol]. One array read.

That the measurement is exactly 1.000 rather than approximately one is worth checking rather than assuming, because the construction runs NFA steps and charges the same counters a query does — so an implementation that did not zero them between the two phases would report a per-character cost that falls as the text lengthens, which is amortisation reported as a step cost.

That is what the first version of this measurement did: 4.85 times more per character at one length than at another, on a machine whose step is a constant. The repair is to separate the two currencies at the point of measurement rather than in the prose.

Why the NFA’s step is forty-four

The set carried by a Thompson simulation over this machine holds several states, and one step visits each of them, tests its predicate, follows the successors of those that match, and closes the result under the free edges.

Measured: 119,266 state visits and 59,636 transitions over four thousand characters — 29.8 and 14.9 a character, summing to 43.7.

That the transitions are exactly half the visits is structural: every split state visited during a closure pushes two successors, and the closure visits each state once.

The machine has 24 states, so a set can hold up to 24 and the measured 29.8 visits a character means the average set is large — most of the machine, most of the time, on a text over an alphabet the expression’s (a|b)* accepts entirely.

There is a second reason the NFA’s step is large on this particular expression and it is worth separating from the machine’s size.

The expression begins with (a|b)*, which accepts every character. So the machine is always in the states corresponding to that prefix, and every character keeps them alive — the set never shrinks to a few.

An expression beginning with a literal would behave differently: after a mismatching character the set collapses to the start state’s closure, and steps become cheap until a match begins.

So 43.7 is a measurement of a machine that is always fully alive, which is the bad case for a set-carrying simulation and the case a prefix-unanchored search is always in. That is the realistic case for regular-expression search — as opposed to a full match — and it is why a search over a long text is where the DFA earns its construction.

Why the bit-parallel machine is not much better

The state set fits in one 32-bit word for this machine, so a step could in principle be a handful of word operations.

It is 37.2, which is 85% of the set-carrying simulation’s.

The reason is the epsilon closure. Thompson’s splits point anywhere, so following the free edges is not a shift — it is a reachability computation over an arbitrary graph, iterated to a fixed point, and each iteration ors a successor mask into the accumulator for every set bit.

So the word representation makes the union cheap and leaves the iteration alone, and the iteration is most of the work.

A construction whose transitions are local — Glushkov’s, with no epsilon edges at all — is what a genuinely bit-parallel matcher uses, and its step is a masked shift. This collection builds Thompson’s, so its bit-parallel machine is a constant factor rather than the technique the literature means. Two states per operator records that choice and its consequence.

Two states per operator, and no rule that copies a sub-machineThompson's construction for (a|b)*a(a|b)(a|b)(a|b): 15 states, of which 9 test a character and 5 split without reading one. The dark edges are the ones a character crosses and the pale edges are followed for free. Every rule of the construction adds a bounded number of states to the machines it is given and no rule duplicates one, which is why the machine is linear in the expression and is built in linear time — and why the exponential that shows up later is a property of the SUBSET construction rather than of the language. A step advances the whole set of states the machine could be in, so a character costs the set's size and not one lookup.0a1b2split3split4a5a6b7split8a9b10split11a12b13split14accept9 character tests · 5 splits · 1 accepting(a|b)*a(a|b)(a|b)(a|b)15 states
Fig. 3 Why the closure is not a shift: the machine’s split edges point anywhere, so following them is a reachability walk rather than a local move.

The lazy machine, which is not a constant

Three of the four have a per-character cost that does not depend on the text’s length. The fourth does, and by a factor of nearly nine.

At 128 characters: 50.9 operations a character. At 512: 49.1. At 2,048: 33.5. At 16,384: 5.8.

The lazy DFA builds a subset state the first time a character asks for it. Early in a text almost every step is a miss — an NFA step plus the bookkeeping. Later, most steps are hits — one lookup.

So its cost per character falls as the text lengthens, and the number quoted for it is a number about a text length.

That is the two-ended check this measurement carries: the NFA, the DFA and the bit-parallel machine must each be flat within 15% across a factor of a hundred and twenty-eight in text length, and the lazy machine must not be. Measured spreads: 1.03, 1.00, 1.03, and 8.74.

A check requiring all four to be flat would fail correctly and would be testing a claim nobody makes. One requiring only the first three would pass on a lazy machine that happened to be flat, which would mean its cache was thrashing at every length.

The lazy construction defers the exponential rather than removing itHow many of the 512 subset states a random text actually causes to be built, against the text's length. At 16 characters the machine holds 17 states and every step is a miss; by 16,384 it holds all 512 and the miss rate has fallen to 9.4%. The lazy DFA is usually described as making the exponential go away — what it does is decide WHEN it is paid, and a text long enough pays all of it. The exponential is a property of the expression; the text chooses only the moment. The dashed line is the whole machine, which nothing in the construction prevents being reached.10010³10⁴100characters of textsubset states built100.0%100.0%97.3%86.4%36.7%9.4%the whole machine: 512the label is theshare of missesk = 8 · alphabet aball 512 by 16,384 characters
Fig. 4 What the lazy machine’s cost depends on: how many of the deterministic machine’s states a text has caused to be built by each length.

The lazy machine’s falling cost is the reach curve read as a cost. At 128 characters it has built seventeen states of five hundred and twelve and every step is a miss; at sixteen thousand it has all of them and the miss rate is 9.4%.

So its per-character cost tracks 1 − (hit rate), scaled by what a miss costs relative to a hit. A miss is an NFA step — about forty operations — and a hit is one lookup, so the cost is roughly 40·(miss rate) + 1.

At a miss rate of 9.4% that predicts 4.8 and the measurement is 5.8, which is the bookkeeping the model omits.

That the model is close enough to be worth writing is what makes the lazy machine’s behaviour predictable from the reach curve, and the reach curve is a property of the expression and the text rather than of the implementation.

What the machines are made of

The four differ in what they carry between characters, and that is the whole of the classification.

The NFA carries a set of states — up to m of them.

The DFA carries an integer — the index of a set.

The lazy DFA carries an integer and a cache of the sets it has named.

The bit-parallel machine carries a word — the set, packed.

So the DFA has traded space for the ability to carry one number, and the other three carry the set in three representations. The exponential is in the expression is where the space that trade costs is measured: 512 states for a twenty-character expression, and unbounded in general.

What the four agree about

Before any of the costs, the four have to be four machines for one language, and the check is that they agree.

Five expressions — a(b|c)*d, (a|b)*abb, [a-c]+d?, a*b*c*, and the blowup family at k = 3 — sixteen texts each, over a four-symbol alphabet. Eighty comparisons per machine, and a backtracking matcher as a fifth opinion.

All five agree on all eighty.

That is worth more than it looks because the five are built by genuinely different routes. The NFA is Thompson’s construction; the DFA is the subset construction over it; the lazy DFA builds the same subsets on demand and throws them away; the bit-parallel machine builds mask tables from the same states; and the backtracking matcher does not use the machine at all — it walks the parse tree.

So a disagreement would localise a bug to one of five independent pieces of machinery, and agreement across all five is a strong statement that the parse and the construction are right.

That fifth opinion is why the backtracking matcher is in the code at all. It is the slowest thing here by orders of magnitude and it is the only implementation that does not share the machine, which makes it the reference the other four are checked against.

What the numbers are per character of

The unit needs saying because “operations” covers four different things across the four machines.

For the NFA: a state visit or a transition followed.

For the DFA: a table lookup.

For the lazy DFA: a lookup on a hit, or an NFA step’s worth on a miss.

For the bit-parallel machine: a word-sized or, counted once per word per operation.

None of those is the same amount of real work, and this collection’s habit requires saying so. A table lookup is a memory read that may miss cache; a state visit is a predicate test on a small object; a word or is a register operation.

So the ratios here are ratios of operation counts and would not be ratios of time. The comparison that is not one comparison is the essay this habit came from, and the direction it cuts is against the DFA: one table lookup into a five-hundred-state table is a memory access with poor locality, and forty word operations on a register are not.

Where building the whole table stops being a wasteTotal operations against the text's length, for one expression whose DFA has 64 states. The DFA's line starts at 5,065 — the construction, paid before the first character — and then rises by one lookup a character. The NFA's line starts at nothing and rises by the size of its state set. They cross at 256 characters, and that crossing is what an engine's "should I compile this" decision actually is: below it the table is wasted work and above it the table is the whole answer. By 16,384 characters the DFA has done 30x less work.10010³10⁴10³10⁴10⁵characters of textoperations, construction includedcrosses at 256the NFAthe DFA64 DFA statescrosses at 256 characters
Fig. 5 The two currencies combined: total operations against the text’s length, where the construction is a fixed charge and the steps are a slope.
A cache below the reachable set is worse than no cache at allOperations per character against the cache's size, for a machine whose text reaches 512 subset states. Every cache under that thrashes: the machine fills it, throws it away and starts again — 1,020 times at a cache of 4 — so every character costs an NFA step plus the bookkeeping, which is 52.7 against the plain NFA's 52.5. The line is flat across two orders of magnitude of cache size and then falls off a cliff at 512, where the reachable set finally fits and the cost drops to 19.9. There is no gentle trade here: the cache either holds the machine the text needs or it holds nothing useful.10100states the cache holdsoperations a characterthe plain NFA: 52.5the whole set fits: 19.94,096 characters · k = 8the knee is at 512 states
Fig. 6 The lazy machine’s other dependence: operations per character against how many states its cache holds, with a cliff where the reachable set fits.

Why the flat lines have to be checked

Three of the four machines have a constant per-character cost and checking that they do is not a formality, because the first version of this measurement found one of them not being constant.

The DFA’s cost came out 4.85 times larger at 512 characters than at 4,096 — a machine whose step is one array read, apparently getting cheaper as the text lengthened.

The cause was the construction: it runs NFA steps, which charge the same counters a query charges, and the counters were not zeroed between the two phases. So the construction’s eleven thousand operations were being divided by the text’s length and reported as a step cost.

That is amortisation reported as a step cost, and it is invisible at any single text length. A measurement taken at one length would have reported a plausible number — 3.7 at four thousand characters — and nothing about it would look wrong.

What found it was sweeping the text length and requiring flatness. A quantity that ought to be constant and is not is a quantity with a term in it, and the term here was the construction leaking into the query.

The comparison that is not one comparison is the essay this habit came from, and this is the same lesson about a counter rather than about a unit: a counter shared between two phases measures both unless something separates them.

What each machine is for

The measurements sort the four into uses.

The DFA is for an expression matched against a lot of text. One lookup a character is unbeatable and the construction amortises.

The NFA is for an expression matched once against a little. No construction beyond the machine itself, and a bounded cost per character.

The lazy DFA is for the case nobody can predict, which is most of them. It converges on the DFA’s cost given enough text and never pays the DFA’s construction up front — provided its cache is large enough, which a cache below the reachable set is about.

The bit-parallel machine is for a machine that fits in a word or two and an expression whose construction has no epsilon edges. Neither holds here, and its 15% is what a Thompson machine gives.

What a fifth machine would look like

The four measured here are the four this collection builds, and there is a fifth in every production engine that is worth naming because it changes the picture.

A hybrid: run the lazy DFA, and if its cache thrashes or its state count exceeds a cap, fall back to the NFA simulation for the rest of the text.

That is what a real engine does and its cost is the minimum of two of the four lines, plus a branch. It never pays the full DFA’s construction, never thrashes indefinitely, and is never much worse than the better of the two.

It is not built here for the reason this collection generally gives about adaptive structures: an adaptive structure hides the boundary it adapts across, and the boundary — the cache size at which thrashing starts, the text length at which the table pays — is what the measurements are for.

A hybrid measured on its own would report one line that is always reasonable and would say nothing about where its two components cross. Where the table starts paying and a cache below the reachable set are the two crossings, and a hybrid’s implementation needs both.

The folklore is about a matcher, not about a languageSteps against the text's length for (a|a)*b matched against a run of a's, on two matchers. The backtracking matcher explores the ways the expression can be laid against the input and doubles with every character: 2,046 at 8 characters and 524,286 at 16. Thompson's NFA advances a state set once per character and costs 137 steps at the shortest length and 329 at the longest — linear, and flat per character. At 20 characters the two are 12,158x apart. The open points are where the backtracking matcher hit the 4,000,000-step cap and was stopped, which is a measurement rather than a gap: the expression is twenty characters long and the text is twenty a's.1010³10⁴10⁵10⁶characters of textstepsbacktrackingThompson's NFAopen points: capped at4,000,000(a|a)*b12,158x at 20 characters
Fig. 7 A machine that is not on the plate and is what most engines actually ship: a backtracking matcher, against Thompson’s NFA, on the standard witness.

The construction costs, side by side

Worth collecting because they span two orders of magnitude and the plate’s bars do not.

NFA: 24 operations — one per state added.

Bit-parallel: 24 for the machine plus a mask table built in one pass over the states, charged as words.

Lazy DFA: 24 for the machine and then whatever the text demands, spread through the run.

Full DFA: 11,297 — 64 states, each requiring an NFA step per alphabet symbol, each of those a set walk with closures.

So the DFA’s construction is 470 times the NFA’s, for a machine 2.7 times larger in states and 44 times cheaper per character.

That ratio is the whole trade and it is a ratio a system can compute before deciding: the construction is O(S·σ·m) for S states, and S is what the subset construction discovers.

Which is the awkward part. S is not known until the construction has run, so a system deciding whether to build a table cannot cost the decision without making it — and that is why the lazy machine, which finds out as it goes, is what production engines ship.

The four numbers, and their conditions

Ending with what a reader should carry, and every one of them has a condition attached.

43.7 operations a character for a set-carrying simulation — on a 24-state machine, over a text the expression’s prefix accepts entirely. Proportional to the machine’s size and to how much of it stays alive.

1.0 for a table lookup — always, on any machine, at any text length. The only unconditional number here.

37.2 for a word-packed set — on a machine fitting in one word, with a Thompson construction’s epsilon edges. On a Glushkov construction it would be a handful.

Between 5.8 and 50.9 for a lazy table — depending entirely on the text’s length and the cache’s size, which is two conditions rather than one.

And 11,297 against 24 for the construction, which is the number that decides everything else and which is a function of a state count nobody knows in advance.

What a character costs, and what was paid before the first oneThe four machines on 16,384 characters against (a|b)*a(a|b)(a|b)(a|b)(a|b)(…, with the construction charged separately from the steps. The NFA costs 52.6 operations a character, which is the size of the state set it is advancing. The DFA costs exactly 1 — one table lookup — and paid 54,311 operations to build 512 states first. The bit-parallel simulation costs 45.1, which is the same state set advanced in 1 machine words: a constant factor on the NFA rather than a change of class, because Thompson's splits make the epsilon closure an iterated or over the whole word rather than a shift.Thompson's NFA, a set of states52.630 to buildthe subset DFA, built in full1.054,311 to buildthe subset DFA, built on demand52.430 to buildthe state set in machine words45.130 to buildoperations per character; the note is what was paid once, before the first character16,384 characters · k = 852.6 down to 1
Fig. 8 The same four machines at four times the text length, where the lazy machine has converged and the other three have not moved.

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.

AutomatonBit-parallelDeterministic automatonEpsilon closureLazy constructionNondeterministic automatonOperation count