[RFC] Add construction-time hash-consing for Expr (hashConsExplore) - #1085
[RFC] Add construction-time hash-consing for Expr (hashConsExplore)#1085gustavo-grieco wants to merge 2 commits into
Conversation
The symbolic interpreter rebuilds structurally-identical subterms as separate heap objects, so a computation that reuses an intermediate at k nesting levels materializes a 2^k tree with no sharing. On storage-heavy targets this exhausts memory: observed, a 914-node DAG materialized as a 601,664-node tree with a 22 GB peak. New EVM.HashCons module: internExpr returns a canonical representative per structural equivalence class (StableName shortcut for already- canonical nodes, shallow structural keys otherwise), so the heap holds the DAG, not the tree. Interning hooks at the term-construction sites (stack ops, SLOAD/keccak paths, mapExpr rebuilds). Gated by the new hashConsExplore config flag, off by default; when disabled every hook is a single IORef read. Canonicalization makes physical identity a cheap witness for structural equality, exploited in two places: * Expr's Eq/Ord instances are now hand-written with a ptrEq fast path at every recursive step (semantics identical to the derived ones), so comparisons on shared terms cost O(differing part), not O(logical tree). * untilFixpoint detects convergence by pointer identity, and the repeatedly-applied simplifier passes (mapExprShared/memoFixTraverse) memoize per-node fixpoint status per pass, skipping already-simplified shared subterms in O(1). Also a readStorage fast path: two distinct literal slots can never alias, so reads skip down concrete SStore chains without invoking the simplifier via surelyEqual/surelyNotEqual. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
97bb439 to
e16e662
Compare
New --hash-cons flag wiring hashConsExplore into the CLI config (still off by default). All test suites (test, clitest, BlockchainTests, rpc, and the forge-symbolic-tests runner via --hash-cons) now run with hash-consing enabled so the sharing machinery is exercised by the whole suite while remaining opt-in for users. resetHashCons now preserves the id counter across resets: ids are never reused, so when parallel test cases trigger overlapping explorations, a mid-run reset can only lose sharing, never correctness (stale memo entries reference ids no new node can be assigned). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the invitation to look at this, @gustavo-grieco. Short version: I did not find anything that changes a verification result. The central claim, that Findings1. The intern tables retain every node ever built, not the live set. On the motivating case the 601664:914 sharing factor pays for that easily. On a workload whose sharing factor is below roughly 10, turning the flag on should raise peak memory, which is the opposite of what the flag is for. Compounding it: because the flag is never written back to Consider disabling and clearing on exploration exit as well as entry, and consider a size or node-count ceiling above which interning quietly stops rather than continuing to accumulate. 2. Concretely, a contract doing N concrete 3. The flag is a process-global with last-writer-wins semantics, and the suites run concurrently. 4. Three of the four suites that set the flag do not actually exercise it.
That leaves 5. There is no direct test for the 556-line module. No property test for 6. One thing I couldn't settleDoes
Smaller notes
|
Description
The symbolic interpreter rebuilds structurally-identical subterms as separate heap objects (e.g. a mapping-slot keccak recomputed on every storage access), so a computation that reuses an intermediate at k nesting levels materializes a 2^k tree with no sharing. On storage-heavy targets this exhausts memory: in one observed case a 914-node DAG materialized as a 601,664-node tree with a 22 GB peak, and every structural traversal or comparison pays the logical (not the shared) size.
This PR adds an experimental, off-by-default fix:
EVM.HashConsmodule —internExprreturns a canonical representative per structural equivalence class, so the heap holds the DAG, not the tree. It is exact (never merges structurally-different terms) and O(1) amortized per node: aStableNametable shortcuts already-canonical nodes, and the structural tables are keyed by shallow keys (constructor tag, scalar payload, child ids), so lookups never compare whole subterms. Interning hooks sit at the term-construction sites: stack ops, the SLOAD/keccak paths, andmapExprrebuilds.Expr'sEq/Ordinstances are now hand-written with aptrEqshortcut at every recursive step. Semantics are identical to the previously-derived instances (constructor declaration order, fields left-to-right); on shared terms comparisons drop from O(logical size) to O(size of the differing part), which removes the dominant cost of simplifier guards (a == b) and normalization compares.untilFixpointdetects convergence by pointer identity, and the repeatedly-applied passes (litToKeccak,simplifyNoLitToKeccak,concKeccakSimpExprviamapExprShared/memoFixTraverse) remember per-node fixpoint status per pass, skipping already-simplified shared subterms in O(1).readStoragefast path — two distinct literal slots can never alias, so reads skip down concreteSStorechains without invoking the simplifier throughsurelyEqual/surelyNotEqual.Everything is gated behind the new
hashConsExploreconfig flag (defaultFalse): when disabled, every hook is a singleIORefread returning its argument unchanged, so existing behavior is untouched. The intern tables are module-global in the style of GHC's FastString table and are reset at the start of each symbolic exploration; the id counter survives resets, so overlapping explorations (parallel test cases) can only lose sharing, never correctness.internExpr eis always structurally equal toe— it only shares memory — so enabling the flag cannot change verification results, only their cost.CLI & tests: the flag is exposed as
--hash-cons, and all test suites (test,cli,BlockchainTests,rpc, and theforge-symbolic-testsrunner) run with hash-consing enabled, so the sharing machinery is exercised by the entire suite while remaining opt-in for users.Checklist
🤖 Generated with Claude Code