perf: flat DFA + integrated prefilter — 35% faster than baseline#151
Merged
perf: flat DFA + integrated prefilter — 35% faster than baseline#151
Conversation
…lete IsComplete() guard blocked prefilter candidate loop for ALL incomplete prefilters, including prefix-only ones where all alternation branches are represented. This caused 22x regression on Kostya's errors pattern (1984ms vs 90ms on v0.12.14). Root cause: Rust integrates prefilter as skip-ahead INSIDE PikeVM (pikevm.rs:1293-1299), not as external correctness gate. When NFA states are empty, prefilter skips ahead. Partial coverage is safe because NFA continues scanning if prefilter misses. Fix: Added partialCoverage flag on literal.Seq (set only on overflow truncation). NFA candidate loop uses !partialCoverage guard instead of IsComplete(). DFA paths retain IsComplete() where needed. errors: 1984ms -> 109ms. Stdlib compat: 38/38 PASS.
Integrate prefilter inside PikeVM search loop as skip-ahead (pikevm.rs:1293). When NFA has no active threads, PikeVM jumps to next candidate via prefilter.Find() instead of byte-by-byte scan. Safe for partial-coverage prefilters — NFA processes all branches from each candidate position. This is architecturally cleaner than external candidate loop guards (partialCoverage flag still used for external BT candidate loop as BoundedBacktracker has no integrated skip-ahead). Also includes PR #150 changes: partialCoverage flag on literal.Seq, NFA candidate loop guard uses partialCoverage instead of IsComplete(). errors pattern: 1984ms -> 120ms. la_suspicious: 38/38 stdlib PASS.
Replace double indirection (stateList[id].transitions[class]) with flat transition table (flatTrans[sid*stride + class]) in searchFirstAt hot loop. Also replace State.IsMatch() with compact matchFlags[sid] bool slice. Fast path now works with state ID only — no *State pointer needed. State struct accessed only in slow path (determinize, word boundary). Inspired by Rust regex-automata hybrid/dfa.rs Cache.trans flat layout. Kostya benchmark: 3.60s -> 2.56s (1.4x faster). bots pattern restored to v0.12.14 baseline (278ms vs 287ms). Stdlib compat: 38/38 PASS.
Unroll DFA hot loop 4x — process 4 bytes per iteration when all transitions are in flat table (no unknown/dead states). Falls to single-byte slow path on any special state. Marginal improvement on x86 with SIMD prefilters (branch predictor handles single-byte well). May help more on ARM64 where branch prediction is less aggressive. Reference: Rust hybrid/search.rs:195-221. Stdlib compat: 38/38 PASS.
Extend flat table optimization from searchFirstAt to all 6 DFA search functions: searchAt, searchEarliestMatch, searchEarliestMatchAnchored, SearchReverse, SearchReverseLimited, IsMatchReverse. Hot loop pattern: ft[int(sid)*stride + classIdx] replaces stateList[id].transitions[class] — eliminates pointer chase. State struct accessed only in slow path (determinize, word boundary). Kostya benchmark: 2.56s -> 2.28s (+12%). errors pattern: 109ms -> 81ms (better than v0.12.14 baseline 90ms). Stdlib compat: 38/38 PASS.
IsComplete() guard in findIndicesDFA/findIndicesDFAAt blocked prefilter skip-ahead for incomplete prefilters (memmem, Teddy with prefix-only literals). But DFA verifies full pattern at candidate — skip is always safe. This was the root cause of sessions (229ms -> 36ms), api_calls (245ms -> 95ms), post_requests (259ms -> 114ms) regressions. Kostya benchmark total: 2.28s -> 1.62s (FASTER than v0.12.14 baseline 1.80s!). Stdlib compat: 38/38 PASS.
When DFA returns to start state with no match in progress, use prefilter to skip ahead to next candidate instead of byte-by-byte scanning. Applied to searchFirstAt and searchAt (bidirectional DFA path). This is the Rust approach (hybrid/search.rs:232-258): prefilter is called inside the DFA loop when a start state is detected, not externally. peak_hours: 197ms -> 90ms (2.2x faster, gap vs Rust: 9x -> 4x). Kostya total: 1.62s -> 1.38s (15% faster). Stdlib compat: 38/38 PASS.
Apply flat table to SearchAtAnchored — called for every prefilter candidate verification in bidirectional DFA path. Eliminates pointer chase in the most frequent DFA hot path. Kostya benchmark: 1.38s -> 1.17s (15% faster). Total improvement vs v0.12.14: 1.80s -> 1.17s (35% faster). Stdlib compat: 38/38 PASS.
…refilterAt Apply flat table to last 2 remaining functions with old Transition() calls. No more State pointer chase in ANY DFA hot loop. Kostya benchmark: 1.17s -> 1.19s (stable, tokens 116ms->51ms). All DFA search functions now use flatTrans[sid*stride+class]. Stdlib compat: 38/38 PASS.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Benchmark ComparisonComparing Summary:
|
On 386, int(StateID(0xFFFFFFFF)) = -1 (int is 32-bit). getState and IsMatchState used int(id) for slice indexing, causing panic: index out of range [-1]. Fix: check sid >= DeadState before int cast. DeadState (0xFFFFFFFE) and InvalidState (0xFFFFFFFF) are sentinel values not present in stateList/matchFlags.
On 386, int is 32-bit. int(StateID(0xFFFFFFFE)) = -2, causing negative slice index panic in flat table lookups. Added safeOffset() helper using uint arithmetic (always positive). Replaced all 23 occurrences of int(sid)*stride in hot loops. safeOffset inlines — zero overhead on 64-bit.
uint multiply overflows on 386: uint(0xFFFFFFFE)*uint(20) wraps around. Guard with sid >= DeadState check — returns MaxInt so bounds check fails safely. Normal state IDs (small values) take fast path without branch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Major DFA architecture upgrade — flat transition table, integrated prefilter skip-ahead in DFA and PikeVM, 4x loop unrolling. Rust-aligned architecture.
Kostya benchmark: 3.60s -> 1.17s (3x faster). 35% faster than v0.12.14 baseline.
Performance
stateList[id].transitions[class](2 pointer chases) withflatTrans[sid*stride+class](1 flat array access). Applied to all 8 DFA search functions.prefilter.Find()to skip ahead. Rust approach (hybrid/search.rs:232-258).pikevm.rs:1293). Safe for partial-coverage prefilters.searchFirstAt— process 4 bytes per iteration.Fixed
partialCoverageflag instead ofIsComplete().errorspattern: 1984ms -> 80ms.sessions: 229ms -> 30ms.Results (Kostya benchmark, 7.2MB x 10 iter)
regex-bench CI (EPYC)
Test plan
go test ./...— all passTestStdlibCompatibility— 38/38 PASSgolangci-lint run— 0 issues