Repository navigation
Conversation
Replace the per-hash `%` with Lemire's remainder by direct computation, using a 128-bit multiplier cached in the filter and derived from the bit array length when a filter is built or deserialized. The result equals `hash % capacity` for every `u64` hash, so bit placement and serialization are unchanged. `Debug` is implemented by hand to keep its output identical. Add a unit test against `%` and a digest-based placement test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
I re-measured the change with the repository harness on two CPUs, with the PR cherry-picked onto
So the Haswell result reproduces (and then some) on Skylake-family cores, but on CPUs with a fast 64-bit divider the five multiplies cost about what the division did, and the 32-byte case is slightly slower because hashing already saturates the multiplier port. One more thing from profiling on the Zen 3 box: for
Bit placement is unchanged and the placement digests pass. I made this change on a branch from this PR |
|
@pmcgleenon Thanks for measuring this on Zen 3 and Kaby Lake, and for the follow-up branch. The Zen 3 numbers show the case I could not measure: the gain comes from removing the 64-bit division, so on a CPU with a fast divider the 128-bit reduction costs about as much as the division it replaces, and @tisonkun the |
Bit placement is already checked by the unit test that compares `compute_bit_index` with `%` and by the cross-language serialization fixtures, which deserialize filters built by other implementations and query their items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ultiplies Override `Hasher::write_u64` on `XxHash64` with a small inlined body and an out-of-line slow path, and mark `compute_hash` `#[inline]`. The default `write_u64` went through the fully inlined streaming `write`, which kept LLVM from inlining the hash into the Bloom filter and left a 343-instruction generic path for two 8-byte hashes; it is now 41 instructions. Replace the 128-bit Lemire multiplier with a `CapacityReducer` that divides the shifted hash (always below 2^63) by the capacity with one high multiply and one shift (Granlund and Montgomery, 1994), then a low multiply for the remainder. The result equals `hash % capacity` for every supported capacity, which a unit test now checks up to `MAX_NUM_BITS` without allocating a filter. The double hash stays in `u64`, so 32-bit targets place bits like 64-bit ones. Measured against the PR head with `benchmarks bloom --repeat 10`, medians of three alternating runs (time ratio, below 1 is faster): AMD EPYC 7R13 0.71 for update::u64, 0.91 for update::bytes_32, 0.63 for contains_and_insert_present_u64; Intel i5-7500 0.70, 0.90, 0.65. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@Waterpine I reproduced a silent false negative when an existing wasm32 filter is read after upgrading to this PR. Please keep the old mapping here and handle the compatibility correction separately with a migration plan. This was introduced in Both the new reduction algorithm and the let hash = h0.wrapping_add(u64::from(i).wrapping_mul(h1)) as usize;
self.reducer.remainder((hash >> 1) as u64) as usizeMinimal reproduction: the bytes below were generated by use datasketches::bloom::BloomFilter;
fn main() {
let hex = "0401150003000000292300000000000003000000000000000300000000000000000808040000000000000000000000000000000000000000";
let bytes: Vec<u8> = (0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
.collect();
let filter = BloomFilter::deserialize(&bytes).unwrap();
assert!(filter.contains(&0u64)); // Fails on the PR head.
}The reverse direction also fails on rollback. Old and new filters still pass About splitting the hashing changeSplitting the hashing optimization into a separate PR is fine and would make its validation easier to review, but I do not require a split. Keeping it here is also fine. |
Converting the double hash to `usize` before the shift truncates it on 32-bit targets, and filters serialized there depend on that bit layout. Restore it in front of the new reduction, so bit placement is unchanged on every target.
|
@tisonkun Thanks for catching this and for the reproduction. I've restored the The description no longer claims the 32-bit change; the 32-bit correction is left for a separate change with a migration plan. I'll keep the hashing change in this PR, since the measurements cover the combination. |
Summary
BloomFiltercomputed each bit index ashash % capacity, a 64-bit hardware division per hash function, and hashedu64items through an out-of-line generic function. This PR removes both costs:hash % capacityfor every supported capacity.XxHash64overridesHasher::write_u64with a small inlined body that produces the same hash as the default, andcompute_hashis#[inline], so hashing au64item no longer goes through a 343-instruction generic function.The reduction and the hashing change are by @pmcgleenon.
On a CPU with a slow 64-bit divider (Kaby Lake), the three
bloom::updatescenarios take 0.30 to 0.49 of the time onmain(51% to 70% less time). On Zen 3, which has a fast divider, they take 0.59 to 0.94 (6% to 41% less time). See "Scope of the results".Serialization and bit placement are unchanged on every target.
BloomFiltergrows from 40 to 64 bytes; heap use is unchanged.Results
The ratio is the time with this PR divided by the time on
main; below 1 is faster.bloom::update::u64bloom::update::bytes_32bloom::update::contains_and_insert_present_u64Each ratio is the product of two measurements with the repository harness (
bloom --repeat 10, medians of three alternating runs): the first version of this PR againstmain(86eb143), and the final version against the first version. Zen 3: 0.96 × 0.71, 1.03 × 0.91, 0.94 × 0.63. Kaby Lake: 0.44 × 0.70, 0.54 × 0.90, 0.46 × 0.65.Scope of the results
bytes_32was 3% slower; the single high multiply fixes that, and every measured scenario is now faster thanmainon both CPUs. CPUs with even faster dividers, such as Apple M-series or newer AMD and Intel cores, were not measured.main; the ratios above multiply two measurements. On Haswell (Xeon E5-2660 v3), the first version measured 0.50, 0.60, and 0.54 againstmain(6856e45, 10 paired runs), and query-onlycontainsand inserts into a 12 MB filter measured 0.41 to 0.81. The final version was not measured on Haswell.u64items only; byte slices and strings hash as before. Count-Min and XOR filters also hash throughXxHash64; their hash values are unchanged, and their performance was not measured.Compatibility
Bit placement is unchanged on every target.
compute_bit_indexstill converts the double hash tousizebefore the reduction, which truncates it on 32-bit targets such as wasm32; filters serialized there depend on that layout. The resulting difference from 64-bit builds and from the other DataSketches implementations predates this PR and needs a migration plan, so it is left for a separate change.Checks
cargo x check,cargo x lint,cargo x test, andcargo +1.86.0 x testpass.mainatc344dadis merged into the branch.