Repository navigation
nft: +refactor Specialize the position encoding and the stop test in the multiword post() - #945
Conversation
[Medium impact] Refactors the multiword search logic in the automaton library. The PR appears safe to merge, with a non-blocking request to test the changed public callback branch. Findings
|
9838384 to
8d08047
Compare
|
Rebased onto |
8d08047 to
00bcefd
Compare
…the multiword post() Three of the remaining items of #806, on top of the rank encoding of #926: - One and two used levels get their own instantiation of the position decoding: with one used level the rank is the position itself, two used levels take one division. The used levels of a transition span are looked up through a prefix table of counts instead of iterating all levels, and their stride sums come precomputed as a prefix-sums difference. - When the product of the radices overflows, the positions of the used levels are hash-consed into fixed-width blocks of one append-only arena with cached hashes and stable ids, instead of one unordered_map node with its own vector copy per tuple. A successor is built in reused scratch storage and looked up there, so a tuple is copied only when it is new. - is_in_lang_by_levels() no longer goes through the public early-exit predicate, which decodes every discovered configuration only to compare the positions against the word ends. A protected reaches_final_by_levels_() asks the search to stop at a final zero-level state at the end id. The randomized differential checks of tests/nft/post.cc compare post() and is_in_lang_by_levels() against a plain vector-tuple reference search over level masks selecting every position-encoding path, both jump modes, and a 33-level case whose 2^33 tuples exercise the interned fallback. The public early-exit predicate, now fed positions rebuilt from the used levels, must see exactly the reference's configurations, with zeros on the projected-out levels of noncontiguous masks, under every rank encoding and the arena, and halt the search at a chosen configuration. Part of #806.
00bcefd to
3b44b40
Compare
Part of #806. Implements the remaining unblocked items — the overflow arena (item 5), the 1-/2-level specialisation with a used-levels prefix table (item 6), and the internal rank-based stop hook for
is_in_lang_by_levels(item 4). Item 7 (per-transition precompute) is skipped: it is blocked on #807.What changes
Position policies in
src/nft/nft.cc. The multiwordpost()search is now templated on a position representation over the used levels only (projected-out levels never move, so a configuration no longer stores their zeros):RankedPositions<1>: the rank is the position itself — no decoding at all.RankedPositions<2>: one division per popped configuration; fixed-width tuples arestd::arrays.RankedPositions<std::dynamic_extent>: the generic mixed-radix decode of nft: +feat Rank reading-head positions and move long searches to a visited bitmap in post() #926.InternedPositions: tuples hash-consed into fixed-width blocks of one append-only arena, with cached hashes and an open-addressed id table. A successor is built in reused scratch storage and looked up there, so a tuple is copied only when it is new (item 5 — previously every consuming step copied a fullstd::vectorbefore theunordered_mapprobe, which allocated a node per new tuple).A span's stride sum is a precomputed prefix-sums difference, and its used levels are located through a per-level count table instead of scanning all levels (item 6).
Rank-based stop (item 4).
Nft::is_in_lang_by_levels()used to passpost()an early-exit predicate that compared full position vectors — forcing a decode of every discovered configuration. It now calls the protectedreaches_final_by_levels_(), whose stop test islevels[target] == 0 && (match_prefix || positions_id == end_id) && final.contains(target): no decoding, on ids only. The publicis_early_exit_configpath keeps its behaviour: it still gets the positions of all levels, rebuilt from the used-level tuple with zeros on the projected-out levels.Tests (
tests/nft/post.cc). A plain vector-tuple reference search written from the documented tape semantics is checked againstpost()(result and visited zero-level states) and againstis_in_lang_by_levels()(prefix and full membership) on randomized level-respecting NFTs with jumps, DONT_CARE and EPSILON transitions, under random level masks covering the 0/1/2/many-used-levels paths, plus a 33-level run whose 2^33 tuples force the interned arena, and deterministic tuple-count boundary tests (255^4 fits the rank, 256^4 does not). The publicis_early_exit_configpredicate is checked against the same reference under noncontiguous level masks with one, two and three used levels (the three rank encodings) and a 32-of-33-levels mask (the interned arena): it must see exactly the reference's configurations, with zeros on the projected-out levels, and halt the search at a chosen configuration without being called again. Each new check was shown to bite by temporarily breaking the corresponding code path.Measured effect
Re-measured on top of
devel1ff4da0 (32-bitStateby default since #939, single inlineSymbolPosttarget since #940); the later #871, #947 and #872 change no code on this path in a release build (#947 only regatesOrdVectorassertions).build/release(g++ 16.2,-O3) libraries of this branch and ofdevel, linked into the same scratch harness,taskset -c 2, min of 7 calls per run, min over three alternating devel/PR runs; both binaries return the same result on every workload. All workloads useJumpMode::AppendDontCares, sinceis_in_lang_by_levels()sendsRepeatSymbolto its dedicated worklist and never reachespost():post(), 3 levels, free interleaving of three 12-long words (13³ tuples)post()with anis_early_exit_configpredicate that never stops, 2 levels, 501² tuples (752 502 calls)The overflow fallback is where item 5 pays off most (node-data arenas with cached hashes instead of a vector copy plus an
unordered_mapnode per step/miss), consistent with the issue's 1.5–5× target; the ranked paths gain a smaller constant factor from the fixed-width decodes, and membership additionally skips the decode for the stop test. The public predicate path still rebuilds the positions of all levels per configuration, so it gains least. The devel times of the interned workload vary between runs (44.9–59.6 ms); the PR's between 15.4 and 20.3 ms.Rebase notes
MATA_ASSERTon the state count in the oldpost()with anstd::overflow_errorin 64-bit-Statebuilds; the rebase moves that check intopost_by_words(), after the empty-delta shortcut as ondevel, so it covers everyPositionsinstantiation of the templated searcher.CI note: macOS runners are queue-starved repo-wide, Linux coverage is the gate.
Test-generator note
The randomized differential generator exposed a latent, pre-existing
post()behaviour worth knowing about: for a transition between two states on the same nonzero level (a malformed, not level-respecting NFT), the span is empty — it reads nothing — yet the current-symbol pruning still filters it out by symbol.develbehaves identically (verified against the devel binary on a witness:visited {0,1}vs an exhaustive reference BFS{0,1,2}); this PR does not change that. The generator now only emits well-formed jumps (a whole cycle only from level 0), keeping that out of the differential.