What the libraries do

Where the table starts paying

Five thousand and sixty-five operations before the first character, then one per character. Against nothing before the first character and thirty-nine per character. They cross at two hundred and fifty-six characters, and that crossing is what an engine's compile decision actually is.

Two machines for one expression and two cost structures.

The NFA costs nothing to prepare beyond its own construction and thirty-nine operations a character.

The DFA costs five thousand and sixty-five operations to build and one operation a character.

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. 1 Total operations against the text’s length for one expression whose deterministic machine has sixty-four states. The construction is a fixed charge and the steps are a slope.

The crossing

At 16 characters: NFA 650, DFA 5,081. The table is eight times worse.

At 64: 2,625 against 5,129.

At 256: 9,402 against 5,321. The table wins.

At 1,024: 40,136 against 6,089.

At 16,384: 639,902 against 21,449. Thirty times better.

So the crossing is between sixty-four and two hundred and fifty-six characters, and past it the table’s advantage grows without bound because its slope is thirty-nine times shallower.

The two lines are worth reading as what they are: a line through the origin against a line with an intercept. That is the simplest possible crossing and it has exactly one solution, which is why this plate is a crossing rather than a region.

The NFA’s line is 39n. The DFA’s is 5,065 + n. They meet at n = 5,065/38 = 133.

The measured crossing is between 64 and 256, and interpolating on the log axis puts it near 190 — above the arithmetic’s 133 because the NFA’s per-character cost is 39 at large n and 40.6 at small n, where the state set has not settled.

That the arithmetic predicts the crossing to within a factor of 1.4 is what makes the formula worth having: a system can compute where the crossing is without measuring, from two numbers it has.

The same shape, elsewhere in this collection

A fixed construction cost against a per-unit query cost is one of the commonest shapes in this field and this collection has met it four times, which makes the crossing worth placing rather than treating as new.

An index larger than what it indexes is the same trade for a suffix array: build it once, then every search is logarithmic rather than linear. Its crossing is at a handful of queries rather than at a text length, because the construction is amortised over queries and not over characters.

The sort the library ships has a version too: a run-detection pass costs a scan and buys a merge that may be much cheaper.

What makes the regular-expression case different is that the construction’s cost is not bounded by the input. A suffix array’s construction is linear in the text; a subset construction’s is exponential in the expression, so the crossing can be anywhere and there is no bound that makes the decision safe.

That is the property that forces the lazy design, and it is worth stating as the distinguishing one: a fixed construction cost is fine when it is bounded and requires a fallback when it is not.

What the crossing is a function of

Three things move it and only one is under anybody’s control.

The DFA’s state count, which decides the construction cost. Sixty-four states here; five hundred and twelve at k = 8, which would put the construction at about 45,000 and the crossing at about 1,200 characters.

The NFA’s size, which decides the slope. Twenty-four states here, giving thirty-nine operations a character.

The alphabet, which multiplies the construction — a state’s row is one entry per symbol, so a byte alphabet is 128 times a binary one.

Substituting: the construction is roughly S·σ·m and the NFA’s slope is roughly m, so the crossing is at about S·σ characters.

At S = 64 and σ = 2 that is 128, and the measured crossing is a little above it. At S = 500 and σ = 256 — a realistic expression on real text — it would be 128,000 characters.

That is the useful form. The table pays after about (states × alphabet) characters, and on a byte alphabet with a few hundred states that is over a hundred kilobytes.

Where building the whole table stops being a wasteTotal operations against the text's length, for one expression whose DFA has 16 states. The DFA's line starts at 985 — 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 64 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 28x less work.10010³10⁴10³10⁴10⁵characters of textoperations, construction includedcrosses at 64the NFAthe DFA16 DFA statescrosses at 64 characters
Fig. 2 The same comparison on a smaller expression, where the deterministic machine has sixteen states and the construction is correspondingly cheaper.

Halving the expression’s k quarters the state count and therefore the construction, and it barely moves the NFA’s slope — so the crossing moves left by about a factor of four.

That is the direction the formula predicts and it is worth confirming, because the alternative reading is that the crossing is a property of the machinery rather than of the expression.

It is a property of the expression, through S. Every parameter of the construction — the alphabet, the machine’s size, the state count — enters through the construction’s cost, and the NFA’s slope enters through m.

A regular-expression search over a document, a log line, or a field is almost always under that threshold.

A search over a whole file, a corpus, or a stream is almost always over it.

So the compile decision is not a property of the expression; it is a property of how much text the expression will be run against, which the engine does not know when it compiles.

That is why the decision is deferred, and deferring it is what the lazy construction is.

What the lazy machine does to the crossing

A lazy DFA builds each state the first time it is needed, so its construction cost is spread through the run and only the states the text reaches are paid for.

Its cost is therefore between the two lines: the NFA’s slope while it is missing, and the DFA’s while it is hitting.

Measured on the same expression: 50.9 operations a character at 128 characters — essentially the NFA’s — and 5.8 at sixteen thousand, approaching the DFA’s.

So it never pays the full construction and never has the DFA’s flat cost, and its own crossing against the pure NFA is much earlier: it starts hitting after a few hundred characters, because a short text reaches few states and those few are reused.

That is the whole argument for the lazy machine and it is why every production engine ships one.

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. 3 The two cost structures per character, with the construction charged separately: one machine free at construction and expensive per character, one the reverse.
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 Why the lazy machine’s cost falls: how many of the deterministic machine’s states a text has caused to be built, against its length.

The lazy machine’s cost has a shape the two-line plate cannot show, and it is worth stating because it is the shape a real system actually pays.

It is 40·(miss rate) + 1 per character, roughly, where the miss rate is the fraction of steps that need a state built. And the miss rate falls as the text lengthens because the reachable set is finite: 100% at 128 characters, 86% at a thousand, 37% at four thousand, 9.4% at sixteen thousand.

So the lazy machine’s line is concave — it starts on the NFA’s slope and bends toward the DFA’s — and it never has an intercept, because nothing is built before the first character.

That concavity is why it is never much worse than the better of the two, and it is also why it has no single crossing with either. A concave curve between two lines crosses each once and lies between them everywhere, which is the best possible behaviour for a machine that has to work without knowing the text’s length.

The decision that cannot be costed

There is a circularity in the compile decision worth naming because it explains the design of every real engine.

To decide whether to build the table, a system needs the construction’s cost, which is S·σ·m.

S — the number of reachable subsets — is not known until the construction has run.

So a system cannot cost the decision without making it. Bounding S from the expression is possible only by the exponential, which is useless: a twenty-character expression’s bound is astronomical and its actual S might be eleven.

The available responses are three.

Build it and cap it. Run the construction with a state limit; if it exceeds, throw it away and use the NFA. That costs the capped work on the bad case.

Build it lazily. Never make the decision; let the text decide by reaching states or not.

Guess from the expression’s shape. Count the closures and alternations; an expression with few is likely to have a small S. That is a heuristic and it can be wrong in both directions.

Real engines do the second, with the first as a backstop.

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. 5 The lazy machine’s failure mode, which the crossing does not show: operations per character against how many states its cache can hold.

Why the crossing is not a rule

Every number above is for one expression on one alphabet, and the crossing moves by three orders of magnitude across realistic inputs.

A literal of twenty characters: S = 21, σ = 256, so the construction is about 5,400 NFA steps and the crossing is at a few thousand characters.

The blowup family at k = 8 on a binary alphabet: S = 512, construction about 45,000, crossing about 1,200.

The blowup family at k = 8 on a byte alphabet: S = 512, construction about 5,700,000, crossing about 150,000.

So a system quoting “compile if the text exceeds a thousand characters” has quoted a number about one of those three.

That is the same shape as most thresholds in this collection — the threshold that reaches zero is a filter’s, and where the sparse representation loses is a bit vector’s — and the resolution is the same: the threshold is a formula rather than a number, and the formula’s arguments are measurable.

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. 6 The quantity the crossing depends on and nobody can bound: the number of states the subset construction discovers, against the expression’s size.

What the construction is actually doing

Five thousand and sixty-five operations for sixty-four states is seventy-nine operations a state, and it is worth unpacking because that number is the crossing’s numerator.

Building a state means, for each alphabet symbol, running one NFA step from the state’s subset: walk the subset testing each state’s predicate, collect successors, close under free edges. That is one NFA step per symbol per state.

An NFA step over a 24-state machine costs about forty operations, so two symbols is eighty a state, and sixty-four states is 5,120. The measured 5,065 is that.

So the construction is S·σ NFA steps, and an NFA step is what the NFA simulation pays per character. Building a DFA costs as much as running the NFA over S·σ characters — which is the crossing, derived rather than measured.

That equivalence is worth having because it makes the trade concrete: compiling is running the slow machine over a synthetic text of S·σ characters, in exchange for making the real text free. A system that expects less than that much text should not compile.

Why an engine compiles anyway

Everything above argues for the lazy machine and most engines that use these techniques do compile eagerly in some cases, so it is worth saying when the argument does not apply.

A pattern known at build time. A tokeniser, a protocol dissector, a lexer generator: the expressions are fixed, the construction happens once ever, and the amortisation is over the program’s whole life. Every crossing is passed by a wide margin.

A pattern applied to a stream. A log filter matching a million lines a second is past the crossing within the first second, and the construction is a startup cost.

A pattern whose S is small and provable. A literal, a character class, a bounded alternation — these have deterministic machines of a few states and no risk. A system able to recognise those cheaply can compile them and defer on everything else, which is a heuristic and is what several engines do.

What the argument rules out is compiling an arbitrary user-supplied expression eagerly against an unknown amount of text, which is the case a search interface is in — and which is the case where the exponential is not merely a slowdown but a way for a user to exhaust a server’s memory with fifty characters.

What is not in the crossing

Two costs the plate omits and both matter to a real decision.

Memory. The table is S·σ entries, and a system may be unable to build it regardless of the text’s length. That is a hard constraint rather than a trade, and it is what a state cap is really for.

Cache behaviour. A table lookup into a large table is a memory access with poor locality; a state-set walk over a small machine is a few accesses to a hot object. So the DFA’s one operation and the NFA’s thirty-nine are not thirty-nine to one in time, and the direction is against the table.

That second is the one this collection’s instrument cannot see. The comparison that is not one comparison is the habit that requires saying so, and here the honest statement is that the crossing measured in operations is later in time than in operations — the table’s advantage per character is smaller than thirty-nine.

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. 7 The machine whose steps the construction is made of: a Thompson NFA, one step of which is what a DFA state costs to build per alphabet symbol.

What the plate holds fixed

Every plate in this collection holds things fixed and it is worth saying which, because the crossing moves with all of them.

The expression is one member of the blowup family at k = 5: sixty-four deterministic states, twenty-four non-deterministic ones.

The alphabet is two symbols. On a byte alphabet the construction is 128 times larger and the crossing moves right by the same factor.

The text is uniform random over that alphabet. A text that kept the machine in few states would make the NFA’s slope shallower and move the crossing right; one that kept it in all of them would do the reverse — and this text does the second, because the expression’s (a|b)* prefix accepts everything.

And the measurement is operations rather than time, which as noted cuts against the table.

So the 256 is a number about a specific expression on a specific alphabet against a specific text, and its value is the formula it confirms rather than itself. The exponential is in the expression is where the first of those four is shown to span a factor of forty-six across two families of the same length.

The crossing as a system’s default

Putting the pieces together, what a system should do is not “compile above a threshold” but something narrower.

Always build the NFA. It is linear, it always works, and it is the fallback.

Run the lazy machine. It costs the NFA’s rate while missing and converges on the table’s while hitting, so it is never much worse than the better of the two.

Cap its cache by memory rather than by a state count, and fall back to the pure NFA on a flush loop — because a cache that thrashes is worse than no cache at all, which a cache below the reachable set measures.

And build the full table only for expressions that are known to be reused, where the construction amortises over many texts rather than over one.

That last is the case the crossing on this plate is actually about: an expression compiled once and matched against a million lines is past every crossing, and the plate’s x-axis should be read as total characters matched rather than as one text’s length.

What a reader should compute

Two numbers, both available before anything is built, and a formula.

The NFA’s size, m, which is the expression’s length plus one. That is the per-character slope: about 1.6m operations, from the measured 39 on a 24-state machine.

The alphabet’s size, σ, which the data gives.

Then the crossing is at about S·σ characters, and S is the one thing not available — so the practical form is a decision procedure rather than a formula.

Start lazy. If the machine settles — its miss rate falls below a few per cent — it has found its S, and S·σ is now known and the decision can be made properly for the next text.

If it thrashes, S exceeds the cache and the expression is one the table cannot serve. Fall back permanently.

That is a controller rather than a threshold, and it is the shape a decision takes when its own input is discovered by making it. This collection has one other of those — the boundary that hides the burst is a schedule chosen by a stream — and both share the property that the measurement and the decision are the same act.

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 52.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 54,311 operations to build 512 states first. The bit-parallel simulation costs 45.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 states52.730 to buildthe subset DFA, built in full1.054,311 to buildthe subset DFA, built on demand19.930 to buildthe state set in machine words45.230 to buildoperations per character; the note is what was paid once, before the first character4,096 characters · k = 852.7 down to 1
Fig. 8 What a settled lazy machine looks like: the four machines at a cache large enough for the reachable set, where the lazy one has converged on the table’s cost.

What this makes readable

Essays that name this one as a prerequisite.

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.

AutomatonCrossing pointDeterministic automatonLazy constructionNondeterministic automatonSubset construction