|
3 | 3 |
|
4 | 4 | //! Split block Bloom filters (SBBF) implementation for Vortex. |
5 | 5 | //! |
6 | | -//! This implementation follows the original paper, renaming `bucket` to `block`, |
7 | | -//! with small changes that help the Rust compiler generate optimized, vectorized |
8 | | -//! code for `make_mask`, `add_hash`, and `find_hash`. |
| 6 | +//! This implementation follows the original paper but |
| 7 | +//! with the following noticeable changes: |
| 8 | +//! - Renaming `bucket` to `block`, |
| 9 | +//! - Small changes that help the Rust compiler generate optimized, vectorized |
| 10 | +//! code for `make_mask`, `add_hash`, and `find_hash` |
| 11 | +//! - A different salt order. |
9 | 12 | //! |
10 | 13 | //! [Split block Bloom filters]: https://arxiv.org/pdf/2101.01719 |
11 | 14 |
|
@@ -43,6 +46,20 @@ pub(super) const BLOCK_SIZE: usize = LANES_PER_BLOCK * BYTES_PER_LANE; |
43 | 46 | /// See the [XXH specification](https://github.com/Cyan4973/xxHash/blob/v0.8.3/doc/xxhash_spec.md#step-1-initialize-internal-accumulators). |
44 | 47 | const DEFAULT_SEED: u64 = 0; |
45 | 48 |
|
| 49 | +/// Eight odd constants for multiply-shift hashing. |
| 50 | +/// |
| 51 | +/// They fit in one 256-bit SIMD vector, and the order matches the one |
| 52 | +/// used by the Apache Parquet specification. The paper's example uses |
| 53 | +/// the same values but in a different order. This was not |
| 54 | +/// intentional for having compatibility with Apache Parquet, but remains |
| 55 | +/// as a common-order in implementations. |
| 56 | +/// |
| 57 | +/// It is important to notice that while order doesn't affect validity, |
| 58 | +/// it changes the final bits set in each lane. |
| 59 | +const SALT: [u32; 8] = [ |
| 60 | + 0x47b6137b, 0x44974d91, 0x8824ad5b, 0xa2b7289d, 0x705495c7, 0x2df1424b, 0x9efc4947, 0x5c6bfb31, |
| 61 | +]; |
| 62 | + |
46 | 63 | /// Hash function to use in a bloom filter. |
47 | 64 | /// |
48 | 65 | /// The current options are fast, non-cryptographic xxHash variants. |
@@ -94,7 +111,7 @@ impl TryFrom<u32> for HashFn { |
94 | 111 | } |
95 | 112 | } |
96 | 113 |
|
97 | | -/// Represents a Split block Bloom Filter filter for a single layout zone. |
| 114 | +/// Represents a Split block Bloom Filter for a single layout zone. |
98 | 115 | pub struct BloomPartial { |
99 | 116 | blocks: Vec<[u32; 8]>, |
100 | 117 | hash_fn: HashFn, |
@@ -195,16 +212,10 @@ impl BloomPartial { |
195 | 212 | fn make_mask(&self, hash: u32) -> [u32; 8] { |
196 | 213 | let mut out = [0u32; 8]; |
197 | 214 |
|
198 | | - // Set eight odd constants for multiply-shift hashing |
199 | | - let rehash: [u32; 8] = [ |
200 | | - 0x47b6137b, 0x44974d91, 0x8824ad5b, 0xa2b7289d, 0x705495c7, 0x2df1424b, 0x9efc4947, |
201 | | - 0x5c6bfb31, |
202 | | - ]; |
203 | | - |
204 | 215 | for i in 0..8 { |
205 | 216 | // Shift all data right, reducing the hash values from 32 bits to five bits. |
206 | 217 | // Those five bits represent an index in [0, 31) |
207 | | - let y = hash.wrapping_mul(rehash[i]) >> 27; |
| 218 | + let y = hash.wrapping_mul(SALT[i]) >> 27; |
208 | 219 |
|
209 | 220 | // Set a bit in each lane based on using the [0, 32) data as shift values. |
210 | 221 | out[i] = 1u32 << y; |
@@ -566,6 +577,38 @@ mod tests { |
566 | 577 | assert!(invalid_filter.is_err(), "expect filter to be invalid"); |
567 | 578 | } |
568 | 579 |
|
| 580 | + /// Another regression test for bloom serialization, |
| 581 | + /// but in this case to detect mask salt changes. |
| 582 | + /// It just verifies that a filter's serialized representation remains stable. |
| 583 | + #[test] |
| 584 | + fn serialized_bits_are_stable() { |
| 585 | + let options = BloomOptions::new(NonZeroU32::MIN, HashFn::XxHash3_64); |
| 586 | + let mut bloom_filter = BloomPartial::from(&options); |
| 587 | + |
| 588 | + bloom_filter.insert(b"vortex"); |
| 589 | + |
| 590 | + // Because we have only one block, and this is the only value inserted, |
| 591 | + // these lanes equal its mask: `empty | mask == mask`. |
| 592 | + let expected_lanes: [u32; 8] = [ |
| 593 | + 0x0000_1000, |
| 594 | + 0x0200_0000, |
| 595 | + 0x0000_2000, |
| 596 | + 0x0800_0000, |
| 597 | + 0x0200_0000, |
| 598 | + 0x0000_0040, |
| 599 | + 0x0000_4000, |
| 600 | + 0x0000_1000, |
| 601 | + ]; |
| 602 | + |
| 603 | + let expected_bytes: Vec<u8> = expected_lanes |
| 604 | + .into_iter() |
| 605 | + .flat_map(u32::to_le_bytes) |
| 606 | + .collect(); |
| 607 | + |
| 608 | + let bytes: Vec<u8> = (&bloom_filter).into(); |
| 609 | + assert_eq!(bytes, expected_bytes); |
| 610 | + } |
| 611 | + |
569 | 612 | // Similar to the goldenfile tests, but for hash functions. |
570 | 613 | // |
571 | 614 | // Useful compatibility test to catch an accidental hash-algorithm or seed change. |
|
0 commit comments