Skip to content

FST read-path and search optimizations#28

Open
capemox wants to merge 3 commits into
masterfrom
vellum-opt
Open

FST read-path and search optimizations#28
capemox wants to merge 3 commits into
masterfrom
vellum-opt

Conversation

@capemox

@capemox capemox commented Jul 16, 2026

Copy link
Copy Markdown
Member

Overview

A set of allocation-conscious performance optimizations to vellum's FST read and search paths. The headline change (this branch's newest commit) speeds up lookups and automaton-guided search with no on-disk format change — existing FSTs benefit without a rebuild. Two earlier commits on the branch trim allocations and table sizes in the regexp/levenshtein automatons.

All changes are covered by the existing test suite plus new benchmarks, and pass under -race.

Contributions

1. Cache root state in Reader and skip redundant key lookups in the iterator (4f29369)

Two format-compatible, allocation-free read speedups:

  • Reader root-state cache — the root state is traversed on every Get, so it is now parsed once at Reader construction and the immutable parsed form is reused, letting each lookup skip re-decoding the root.
  • Iterator offset accessor — the automaton-guided iterator already knows a transition's sorted offset from TransitionAt, but then re-searched for the byte via TransitionFor (a bytes.IndexByte scan) to recover the destination/output. A new TransitionDestForOffset resolves destination/output directly by offset, and only after the automaton accepts the transition, so selective searches don't decode transitions they discard.

Benchmarks (Apple M4 Pro, benchstat, n=6, all p=0.002; allocations unchanged at 0 for Get):

Benchmark Before After Δ
GetWords 99.2µs 88.1µs −11.1%
GetWide (8-byte ids) 5.91ms 5.49ms −7.1%
GetWide2 (2-byte, high-fanout) 2.50ms 1.74ms −30.4%
ScanWords 87.3µs 81.9µs −6.2%
ScanWide 7.15ms 6.71ms −6.1%
FuzzyWords1 2.97µs 2.73µs −8.0%
FuzzyWords2 10.6µs 9.6µs −9.3%
RegexWords 80.6µs 74.0µs −8.2%
geomean −11.2%

2. Shrink regexp DFA tables and pool FST automaton states (c953a0a)

Reduces the regexp DFA table footprint and pools the transient fstState instances used by the Automaton/Transducer interface methods, avoiding a per-call allocation.

3. Fix swallowed build error and reduce levenshtein allocations (0469d8d)

Surfaces a previously swallowed build error and trims allocations in the levenshtein automaton construction path.

Notes

  • No FST on-disk format change; the encoding version is unchanged and existing FSTs are read faster as-is.
  • A more invasive, format-changing optimization (a fat-node transition index, format v2) was evaluated and deliberately not included here — it helps only high-fanout exact lookups (~+27%), does little for search, and costs file size plus a format migration. It is preserved on a separate WIP branch for later consideration.

capemox and others added 3 commits June 30, 2026 11:35
Three focused fixes:

1. builder.go: compileFrom swallowed errors from compile() via `return nil`,
   silently producing a corrupt FST on a mid-build write failure. Return the
   error instead.

2. levenshtein/alphabet.go: dedupe() built its result via O(n^2) string
   concatenation (rv += string(r)). Use a strings.Builder. Runs per fuzzy query.

3. levenshtein/parametric_dfa.go: getHash() json.Marshal'd each NFAState and
   SHA-256'd the result to key a map[[32]byte]int. Replace with a direct
   6-bytes-per-state encoding into a reused buffer, keyed in a map[string]int.
   No reflection, no hashing, exact (collision-free) keys. Drops the
   crypto/sha256 and encoding/json imports.

Added BenchmarkNewLevenshteinAutomatonBuilder1/2 to cover the construction
path. Building an edit-distance-2 automaton is now ~3x faster with ~58% fewer
allocations; per-query BuildDfa drops ~8-10% allocations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#4 regexp/dfa.go: state.next was []int (256*8 = 2KB per state on 64-bit).
   State indices are bounded by StateLimit (10000), so []uint32 is always
   sufficient and halves the transition-table footprint. For DFAs near the
   state cap this saves ~10MB. Total compile allocation drops 15-25% on the
   benchmark regexps, entirely attributable to the smaller tables.

#5 fst.go: the Automaton/Transducer interface methods (Accept/AcceptWithVal,
   IsMatch/IsMatchWithVal) called decoder.stateAt(addr, nil), allocating a
   throwaway fstStateV1 on every call. TransducerGet over an 11-byte key thus
   allocated 12 states. Recycle states through a sync.Pool on the FST (safe
   for concurrent use). TransducerGet goes from 1920 B/12 allocs to 0/0 and
   ~18% faster; the iterator and Reader paths are unchanged (they already
   reuse a prealloc state).

Adds DFA-footprint and read-path benchmarks plus a concurrency race test for
the pooled methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ator

Two allocation-free, format-compatible speedups to FST reads (no on-disk
format change; existing FSTs benefit without a rebuild):

1. Reader root-state cache: the root is traversed on every Get, so parse
   it once at Reader construction and reuse the immutable parsed form,
   letting each lookup skip re-decoding the root.

2. Iterator offset accessor: the automaton-guided iterator already knows a
   transition's sorted offset from TransitionAt, but then re-searched for
   the byte via TransitionFor (a bytes.IndexByte scan) to recover the
   dest/output. Add TransitionDestForOffset to resolve dest/output directly
   by offset, and only after the automaton accepts the transition so
   selective searches don't decode transitions they discard.

Benchmarks (Apple M4 Pro, benchstat, n=6, all p=0.002; 0 allocs unchanged):

  GetWords       99.2µs -> 88.1µs   -11.1%
  GetWide (8B)   5.91ms -> 5.49ms    -7.1%
  GetWide2 (2B)  2.50ms -> 1.74ms   -30.4%
  ScanWords      87.3µs -> 81.9µs    -6.2%
  ScanWide       7.15ms -> 6.71ms    -6.1%
  FuzzyWords1    2.97µs -> 2.73µs    -8.0%
  FuzzyWords2    10.6µs ->  9.6µs    -9.3%
  RegexWords     80.6µs -> 74.0µs    -8.2%
  geomean                            -11.2%

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant