Index the structural hash tables by mask rather than remainder - #12
Open
TrevorHansen wants to merge 1 commit into
Open
Index the structural hash tables by mask rather than remainder#12TrevorHansen wants to merge 1 commit into
TrevorHansen wants to merge 1 commit into
Conversation
Aig_Hash() and Gia_ManHashOne() both end in "Key % TableSize", with TableSize a prime read out of the manager on every call. That is a hardware divide by a runtime value on the hottest path either package has. Profiling a bit-blasted query whose conversion is dominated by AIG construction, Aig_TableInsert(), Aig_TableLookup() and Aig_TableResize() are 50.8% of the whole run between them. Size both tables to a power of two and reduce the key by multiplying it by an odd 64-bit constant, taking bits out of the high half of the product, where every input bit has had an effect. The mixing steps are untouched, and so is the growth policy: both tables still grow once the entries reach twice the buckets, and still grow to hold twice the entries -- rounded up to a power of two rather than up to a prime. Occupancy does not suffer. Over 400,000 keys drawn the way an AIG draws them -- dense fanin ids at small offsets, both polarities -- each table leaves as many buckets empty as a uniform hash predicts for the load it is at: 36.8% for the prime table at load 1.00 and 46.7% for the power-of-two table at load 0.76, against predictions of 36.8% and 46.8%. Rounding the size up rather than to a prime also leaves the table a little emptier, which is where the shorter chains come from. Structural hashing gets 3.1% to 23.4% cheaper on three large queries at two conversion levels of a bit-blasting client, read off that client's own bit-blasting timer and interleaved against the same build without the change. Retired instructions are flat to within 0.6% either way, which is the point: one divide becomes a multiply, a shift and an and. Peak memory is unchanged on two of the three and 5.7% higher on the third, because the power of two above twice the entries can be nearly twice the prime above it. Sizing down to the power of two below instead gives that back, but costs most of the speed -- it leaves the table between one and two times the entries rather than two to four, so the manager rehashes half again as often -- so the size is rounded up. 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: over 97 queries, across six conversion levels and a fuzz corpus, the CNF derived afterwards is byte-identical to the CNF derived before.
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.
Aig_Hash()andGia_ManHashOne()both end inKey % TableSize, withTableSizea prime read out of the manager on every call. That is a hardware divide by a runtime value on the hottest path either package has. Profiling STP converting a large bit-blasted query,Aig_TableInsert(),Aig_TableLookup()andAig_TableResize()are 50.8% of the whole run between them.This sizes both tables to a power of two and reduces the key by multiplying it by an odd 64-bit constant, taking bits out of the high half of the product. The mixing steps are untouched, and so is the growth policy: both tables still grow once the entries reach twice the buckets, and still grow to hold twice the entries — rounded up to a power of two rather than up to a prime.
Does the mask distribute as well as the modulus?
Yes. Over 400,000 keys drawn the way an AIG draws them — dense fanin ids at small offsets, both polarities — each table leaves as many buckets empty as a uniform hash predicts for the load it is at.
% primeThe probe difference is the load, not the function: rounding a size up to a power of two rather than to a prime leaves the table a little emptier than it was. Feeding the same test a fully structured stream — consecutive ids at fixed small offsets, which is what a bit-blaster actually produces — gives the same figures to a tenth of a percent, so the mask is not sensitive to that regularity.
Speed
Measured on STP's own bit-blasting timer, interleaved against the same build without the patch, arm order rotated per round, medians of five reps (nine for the last row). Three large queries at STP's two cheapest conversion levels.
Whole-run CPU cycles to the point where the CNF is complete move −2.0% to −7.0% across the same six, and retired instructions move by −0.63% to +0.08%. The instruction count staying flat is the point: one divide becomes a multiply, a shift and an and, so counting instructions scores this change as neutral-to-worse and only cycles show it.
Part of the gain is not the divide. Because a power of two above
2nis at least the prime above2n, the manager has to grow further before the next trigger and so rehashes less often: on the first query above,Aig_TableResizefalls from 9.3% of the run to 4.7%.Memory
The same rounding is the cost. The power of two above
2ncan be nearly twice the prime above2n, so where a run's last resize lands just past a boundary the table doubles.Rounding down instead bounds the table at
2nand takes that +5.7% to +1.2%, but it leaves the table between one and two times the entries rather than two to four, so the manager rehashes about half again as often: it gives back most of the speed and turns the fourth row above into a +6.8% regression. Rounding up is the better trade.Tried and dropped: growing at load factor one
The obvious companion change — resize when the entries reach the buckets rather than twice the buckets — is a loss, by up to 16.9% of cycles, and is not in this PR.
Both tables oscillate between load 0.5 and load 2, so the mean load is 1.0, not 2.0, and the mean successful probe is 1.50. Growing at load 1 only takes that to about 1.36, while
nthen doubles rather than quadruples between resizes, so the manager makes about 2 full rehash passes over its lifetime instead of about 1.33.perfshows the trade directly:Aig_TableResizegoes from 9.3% to 15.3% of the run whileAig_TableInsert(25.2% → 21.9%) andAig_TableLookup(16.3% → 13.0%) both fall. Fifty per cent more rehashing for nine per cent fewer probes.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 12 large queries at each of those six levels plus 25 fuzz-corpus queries at the default level, the emitted CNF is byte-identical to the CNF the same build without the patch emits — 97 of 97, no mismatches and no skips. The same 97 were also byte-identical for the load-factor variant above before it was dropped on speed, which is the stronger test of the ordering argument since it reshuffles the buckets on a different schedule as well.
Both arms of that comparison are the same STP revision, built in the same tree, differing only by this patch, and each is reached through a wrapper that pins it to its own
libstpso neither can pick up the other's.The only places either package walks a table in bucket order are
Aig_TableProfile(),Gia_ManHashProfile()andAig_TableCountEntries()— two debug prints and a count used in an equality assertion — and every reader ofvHTableoutsidegiaHash.conly tests it against zero.Notes
Aig_TableSizeFor()is exported fromaigTable.cbecauseAig_ManStart()needs the same rounding for the table it allocates up front; that is the one line in the header.intthe manager stores them in.