3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-14 19:15:41 +00:00

Improve hash mixing to eliminate bitvector-expression hash-table clustering (#10120)

Large Python bitvector workloads were hitting a sharp performance cliff
during `Solver.add(...)`, consistent with severe hash-table clustering
in expression-heavy assertion paths. The issue was sensitive to input
size/alignment, indicating weak low-bit dispersion in hash combination.

- **Hash mixing update (`src/util/hash.h`)**
- Replaced the old `combine_hash(h1, h2)` arithmetic/xor sequence with
stronger mixing:
    - boost-style combine step
    - `hash_u(...)` finalization
- Goal: improve low-bit entropy used by chained hash-table bucket
selection under aligned/high-volume AST patterns.

- **Regression guard and A/B comparison (`src/test/chashtable.cpp`)**
- Added `tst_combine_hash_low_bits()` and invoked it from
`tst_chashtable()`.
- The test stresses aligned first components (`i << 12`) combined with a
fixed seed.
- Added an in-test comparison between the **old** and **new** pairwise
hash combiners and validates:
- reduced collision counts for low-bit projections (8-bit and 16-bit
suffixes),
    - improved low-bit uniformity for 8-bit and 16-bit suffixes,
- reported prefix/suffix uniformity metrics (high/low 8 and 16 bits) for
visibility in test output.

```cpp
static inline unsigned combine_hash(unsigned h1, unsigned h2) {
    h1 ^= h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2);
    return hash_u(h1);
}
```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot 2026-07-14 11:51:49 -07:00 committed by GitHub
parent c4cb5bbc15
commit 82a0d42970
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 86 additions and 5 deletions

View file

@ -21,6 +21,8 @@ Revision History:
#include "util/hash.h"
#include "util/util.h"
#include <iostream>
#include <cstdint>
#include <vector>
typedef chashtable<int, int_hash, default_eq<int> > int_table;
typedef cmap<int, int, int_hash, default_eq<int> > int_map;
@ -172,6 +174,87 @@ static void tst6() {
});
}
static unsigned combine_hash_old(unsigned h1, unsigned h2) {
// Pre-change combine_hash implementation kept for A/B quality checks.
h2 -= h1; h2 ^= (h1 << 8);
h1 -= h2; h2 ^= (h1 << 16);
h2 -= h1; h2 ^= (h1 << 10);
return h2;
}
constexpr unsigned hash_compare_num_samples = 1u << 16;
constexpr unsigned hash_compare_seed = 0x12345678u;
constexpr unsigned hash_compare_alignment_shift = 12;
struct hash_projection_stats_t {
unsigned m_occupied = 0;
uint64_t m_uniform_error = 0;
};
template<typename CombineHash>
static hash_projection_stats_t get_projection_stats(CombineHash const& combine, unsigned bits, bool low_bits) {
// Project hashes to either a suffix (low bits) or prefix (high bits), then
// collect occupancy and a scaled squared-error uniformity score.
SASSERT(bits <= 16);
unsigned const num_buckets = 1u << bits;
unsigned const mask = num_buckets - 1;
int64_t const signed_num_samples = static_cast<int64_t>(hash_compare_num_samples);
int64_t const signed_num_buckets = static_cast<int64_t>(num_buckets);
std::vector<unsigned> counts(num_buckets, 0);
for (unsigned i = 0; i < hash_compare_num_samples; ++i) {
unsigned h = combine(i << hash_compare_alignment_shift, hash_compare_seed);
unsigned b = low_bits ? (h & mask) : (h >> (32 - bits));
counts[b]++;
}
hash_projection_stats_t stats;
for (unsigned c : counts) {
if (c != 0)
++stats.m_occupied;
// Scaled squared deviation from ideal per-bucket load num_samples/num_buckets.
int64_t diff = static_cast<int64_t>(c) * signed_num_buckets - signed_num_samples;
stats.m_uniform_error += static_cast<uint64_t>(diff * diff);
}
return stats;
}
static void tst_combine_hash_low_bits() {
constexpr unsigned bits8 = 8;
constexpr unsigned bits16 = 16;
auto old_low8 = get_projection_stats(combine_hash_old, bits8, true);
auto new_low8 = get_projection_stats(combine_hash, bits8, true);
auto old_low16 = get_projection_stats(combine_hash_old, bits16, true);
auto new_low16 = get_projection_stats(combine_hash, bits16, true);
auto old_high8 = get_projection_stats(combine_hash_old, bits8, false);
auto new_high8 = get_projection_stats(combine_hash, bits8, false);
auto old_high16 = get_projection_stats(combine_hash_old, bits16, false);
auto new_high16 = get_projection_stats(combine_hash, bits16, false);
unsigned old_low8_collisions = hash_compare_num_samples - old_low8.m_occupied;
unsigned new_low8_collisions = hash_compare_num_samples - new_low8.m_occupied;
unsigned old_low16_collisions = hash_compare_num_samples - old_low16.m_occupied;
unsigned new_low16_collisions = hash_compare_num_samples - new_low16.m_occupied;
ENSURE(new_low8_collisions < old_low8_collisions);
ENSURE(new_low16_collisions < old_low16_collisions);
ENSURE(new_low8.m_uniform_error < old_low8.m_uniform_error);
ENSURE(new_low16.m_uniform_error < old_low16.m_uniform_error);
std::cout << "combine_hash old/new low8 collisions: "
<< old_low8_collisions << "/" << new_low8_collisions << "\n";
std::cout << "combine_hash old/new low16 collisions: "
<< old_low16_collisions << "/" << new_low16_collisions << "\n";
std::cout << "combine_hash old/new low8 uniform_error: "
<< old_low8.m_uniform_error << "/" << new_low8.m_uniform_error << "\n";
std::cout << "combine_hash old/new low16 uniform_error: "
<< old_low16.m_uniform_error << "/" << new_low16.m_uniform_error << "\n";
std::cout << "combine_hash old/new high8 uniform_error: "
<< old_high8.m_uniform_error << "/" << new_high8.m_uniform_error << "\n";
std::cout << "combine_hash old/new high16 uniform_error: "
<< old_high16.m_uniform_error << "/" << new_high16.m_uniform_error << "\n";
}
void tst_chashtable() {
tst1();
tst2();
@ -180,5 +263,6 @@ void tst_chashtable() {
tst4<dint_table>(1000,10);
tst4<dint_table>(10000,10);
tst4<int_table>(50000,1000);
tst_combine_hash_low_bits();
tst5();
}

View file

@ -57,10 +57,8 @@ static inline unsigned hash_ull(unsigned long long a) {
}
static inline unsigned combine_hash(unsigned h1, unsigned h2) {
h2 -= h1; h2 ^= (h1 << 8);
h1 -= h2; h2 ^= (h1 << 16);
h2 -= h1; h2 ^= (h1 << 10);
return h2;
h1 ^= h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2);
return hash_u(h1);
}
static inline unsigned hash_u_u(unsigned a, unsigned b) {
@ -256,4 +254,3 @@ static inline unsigned mk_mix(unsigned a, unsigned b, unsigned c) {
return c;
}