aig, gia: reduce the structural hash key without a divide - #12
Open
TrevorHansen wants to merge 2 commits into
Open
aig, gia: reduce the structural hash key without a divide#12TrevorHansen wants to merge 2 commits into
TrevorHansen wants to merge 2 commits into
Conversation
Aig_Hash() and Gia_ManHashOne() both end in "Key % TableSize", with TableSize read out of the manager on every call: a hardware divide by a runtime value on the hottest path either package has. Profiled over a bit-blasted query, Aig_TableInsert(), Aig_TableLookup() and Aig_TableResize() are between 66% and 80% of AIG construction, and Gia_ManHashAnd() with Gia_ManHashResize() is 86% of the GIA one. Reduce the key with two multiplies instead. Multiply it by an odd 64-bit constant and take the high half of the product, which every input bit has had an effect on, then map that onto the table by multiplying by the table size and taking the high half again. The mixing steps above are untouched, and so is every table size: Abc_PrimeCudd() still chooses it, the growth policy keeps its trigger and its factor, and no table allocates a byte it did not allocate before. A power-of-two table indexed by a mask is the shorter way to drop the divide, and it is not available here. nTableSize and pTable are public fields of Aig_Man_t, and a client that knows how many nodes it is about to build can swap in a table of its own sizing -- which one does, with a prime. A mask then collapses the table onto the buckets the size's set bits reach: over the keys of one such blast, a table of 2,191,451 buckets holding 1,890,878 keys used 512 of them, mean chain 3,693, and the blast did not finish. The reduction here leaves 1,266,734 buckets occupied against the 1,266,744 a uniform hash predicts, and costs 1.432 probes per lookup against the remainder's 1.430. A lookup decides identity by comparing fanins inside the chain walk, and a key is in the table at most once, so neither the bucket a node lands in nor the order within a chain can change which node is found. Checked rather than assumed: across six conversion levels of a bit-blasting client over 52 large queries, and its default settings over 25 fuzzer outputs, all 337 comparisons give a byte-identical CNF. What this does not buy is time. Interleaved against the same build without it on an unloaded machine, three large queries move that client's own bit-blasting timer by +0.33%, -0.28% and +0.93% at the AIG level and by +0.76%, +1.21% and -1.27% at the GIA level, every one of them inside the run-to-run spread, while retired instructions rise between 0.01% and 0.25%. The divide sits on the dependency path to the bucket load, but that load misses cache often enough to hide it. The same three queries do respond to the load factor: sizing the client's table four times larger takes 22% to 26% off the same timer, for 5.6% more peak memory.
TrevorHansen
force-pushed
the
strash-hash-reduction
branch
from
September 1, 2026 06:58
805442c to
480e33e
Compare
Aig_TableResize walked the old bucket chains to refill the new array, so both arrays were live across the resize and the transient was their sum. Every node the table holds is also in vObjs -- the invariant the Counter assert has always stated -- and the hash is recomputed from the fanins either way, so the rebuild can sweep vObjs instead: the old array is freed before the new one is allocated, the transient becomes the larger of the two rather than their sum, and the sweep reads nodes sequentially where chain-chasing did not. Chain order changes, which lookups cannot observe (each key exists at most once); the emitted CNF is byte-identical across the hard-set sample on both generators that drive this table. Peak RSS on real queries is unchanged, because the last resize fires mid-blast at about two thirds of the final node count and the process peak comes later -- the bound matters for callers whose footprint peaks at the resize, and the sequential sweep and earlier free are worth having regardless.
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.
This branch was force-pushed; the earlier mask-and-power-of-two version is withdrawn. It was unsound — see "Why not a mask" below. What is on the branch now is a two-multiply reduction that makes no assumption about the table size.
Aig_Hash()andGia_ManHashOne()both end inKey % TableSize, withTableSizeread out of the manager on every call: a hardware divide by a runtime value on the hottest path either package has. Profiled over a bit-blasted query,Aig_TableInsert(),Aig_TableLookup()andAig_TableResize()are between 66% and 80% of AIG construction, andGia_ManHashAnd()withGia_ManHashResize()is 86% of the GIA one.The change reduces the key with two multiplies instead: multiply by an odd 64-bit constant and take the high half of the product, which every input bit has had an effect on, then map that onto the table by multiplying by the table size and taking the high half again. The mixing steps above are untouched, and so is every table size —
Abc_PrimeCudd()still chooses it, the growth policy keeps its trigger and its factor, and no table allocates a byte it did not allocate before. The diff is two functions.Why not a mask
A power-of-two table indexed by a mask is the shorter way to drop the divide, and it is not available here.
nTableSizeandpTableare public fields ofAig_Man_t, and a client that knows how many nodes it is about to build can swap in a table of its own sizing. STP does exactly that inBBNodeManagerAIG::hintExpectedAnds(), with a prime. A mask then collapses the table onto the buckets the size's set bits reach, and the blast stops finishing: one 62 MB query that converts in 24 seconds was still inAig_TableInsert()andAig_TableLookup()nine minutes later, with noAig_TableResize()samples at all.Dumping the real keys of that blast — 1,890,878 distinct keys into the 2,191,451-bucket table the client installed:
Key % TableSize(today)Output neutrality
A lookup decides identity by comparing fanins inside the chain walk, and a key is in the table at most once, so neither the bucket a node lands in nor the order within a chain can change which node is found. The AIG built is the same one.
Checked rather than assumed. STP derives CNF from these tables by six different routes; over 52 large queries at each of those six conversion levels, plus 25 fuzzer outputs at the default settings, the emitted CNF is byte-identical to the CNF the same build without the patch emits: 337 of 337, no mismatches and no skips. Both arms are the same STP revision built in the same tree, differing only by this patch, and each is reached through a wrapper pinning it to its own
libstpso neither can pick up the other's.The only places either package walks a table in bucket order are the two rehash loops,
Aig_TableCountEntries()(a count, used in an equality assertion), andAig_TableProfile()/Gia_ManHashProfile(), which are debug prints with no live call sites. Every reader ofvHTableoutsidegiaHash.conly tests it against zero.Speed: none
This is the part that argues against merging. Interleaved against the same build without the patch, run by run, medians of seven reps on an idle machine, three large queries at the two cheapest conversion levels:
Every stage figure is inside the run-to-run spread, and retired instructions rise slightly, which is expected: one divide becomes two multiplies and two shifts. The divide sits on the dependency path to the bucket load, but that load misses cache often enough to hide it.
Peak RSS is unchanged, as it must be — nothing here allocates: 1,003,300 → 1,006,436 kB at very-low and 1,028,048 → 1,028,620 kB at gia-low on the largest of the three, both smaller than the spread between two runs of the same binary.
An earlier reading of this branch had it 3% to 23% cheaper. That was the withdrawn version, and the gain was not the mask: rounding a resize request up to a power of two overshoots by 2x whenever the request lands just above one, so that version was quietly running at half the load factor. The load factor is the whole effect. Sizing the client's table four times larger, with the divide left in place, moves the same three queries by −26.0%, −22.3% and −22.5% on the bit-blasting timer, for 5.6% more peak memory — twenty times what removing the divide is worth, and it belongs on the client side, not here.
So: correct, exact, free, and pointless on today's hardware. Worth taking only if a divide-free hash is wanted for its own sake, or as the prerequisite for a client that does want to size its own table.
A second commit rebuilds the table from vObjs on resize instead of walking the old buckets, so the old array is freed before the new one is allocated: the resize transient becomes the larger of the two arrays rather than their sum, and the refill reads the nodes sequentially. Membership is unchanged - the table holds exactly the AND and EXOR nodes, which is the invariant the resize's own Counter assert has always stated. Byte-identical CNF on the hard-set sample under both generators that drive this table, and the full STP suite passes against it. Measured honestly: peak RSS on real queries does not move, because the last resize fires mid-blast at about two thirds of the final node count and the process peak comes later - the bound matters for callers whose footprint peaks at the resize, and the earlier free and sequential sweep are worth having regardless.