Scan encoded expressions without traversal stacks - #138
Conversation
`item_source` and `ExprEnv::args` walked an encoded expression by pushing each pending arity onto a stack (a `SmallVec` in item_source, a per-subterm `traverseh!` in args) and popping as they descended. The encoding is prefix order and every tag says how many subterms follow, so one `pending` counter suffices: `Arity(a)` sets `pending += a - 1`, every leaf (Symbol, NewVar, VarRef) decrements it, and the walk ends when `pending` reaches zero. No stack, no per-item bookkeeping allocation. The item stream is unchanged, so this is byte-identical. A generated differential (`generated_expression_scanners_match_recursive_reference`) checks the pending-count scanners against a recursive reference over 4096 random terms, item for item and arg span for arg span. item_source is the byte-machine's inner scan, so dropping its stack shows up across the suite. Measured on `mork bench`, min-of-3 perf instructions:u, outputs and step counters identical to the parent: counter_machine -28.6% process_calculus -18.8% bfc -13.1% exponential_fringe -12.0% transitive -5.8% odd_even_sort -2.8% clique -1.1%
|
This was on my to list for a long time, but due to the "non-constant factor only" optimization policy I never got to it. The initial plan was to have an expression zipper as well, hence the stack, but let's simplify the traversal! |
|
I guess we can't replace it in traverseh because functions take the item index as an argument (which we're eliding here)? |
|
args should not be used in the places that show up in the benchmarks in the first place: the traversal if the expression is quadratic. coreferential transition for example should (probably) not re derive children during descend. |
|
on what blocks me is the payload. i did go through all five remaining call sites, and they happen to be counter-compatible, because the stack i did miss, and which is worth the same treatment, is on i am not deriving that from nothing, i have hit it once already. on the compile path where i want to be careful is the interpreter path you are pointing at, because i have already tried
both failed for the same reason, and that is the part i think is actually useful here: almost all of which is where i think this PR's observation leads. the encoding is preorder-contiguous, so the Arity
one thing i should correct rather than let stand: the ~11.8% figure i have quoted for |
item_sourceandExprEnv::argswalked an encoded expression with an explicit traversal stack — aSmallVecinitem_source, a per-subtermtraverseh!inargs— pushing pending arities and popping on descent. The encoding is prefix order and every tag carries how many subterms follow, so one integer is enough:Arity(a)doespending += a - 1, each leaf (SymbolSize,NewVar,VarRef) decrements, and the walk ends whenpendingreaches zero. No stack, no per-item bookkeeping. Theitem_sourcehunk is +17/-26, it shrinks the function.item_sourceis the byte-machine's inner scan, so dropping its stack shows up across the whole suite. Measured onmork bench, releasetarget-cpu=native, min-of-3perf stat -e instructions:u, against the parent commit. Outputs and internal step/unification counters are identical everywhere:Byte-identical by construction (the item stream is unchanged), and pinned:
generated_expression_scanners_match_recursive_referenceruns 4096 random terms through the pending-count scanners and a recursive reference, comparing item for item and arg span for arg span.cargo test -p mork-expris green (18 tests),mork testexits 0, andmork bench process_calculusreports the sameunifications 201401, instructions 427949817as the parent. Default build, no feature flags, no interface change.