Skip to main content

polars_utils/
bloom_filter.rs

1//! Split-block bloom filter, the layout of parquet's bloom filters: 32-byte
2//! blocks, and a key sets one bit in each of the eight words of one block.
3//! See <https://github.com/apache/parquet-format/blob/master/BloomFilter.md>.
4
5const SALT: [u32; 8] = [
6    1203114875, 1150766481, 2284105051, 2729912477, 1884591559, 770785867, 2667333959, 1550580529,
7];
8
9const BLOCK_BYTES: usize = 32;
10
11/// Always below `num_blocks`.
12#[inline]
13fn block_index(hash: u64, num_blocks: usize) -> usize {
14    (((hash >> 32) * num_blocks as u64) >> 32) as usize
15}
16
17#[inline]
18fn block_mask(hash: u64) -> [u32; 8] {
19    let key = hash as u32;
20    std::array::from_fn(|i| 1u32 << (key.wrapping_mul(SALT[i]) >> 27))
21}
22
23#[inline]
24fn load_block(bytes: &[u8; BLOCK_BYTES]) -> [u32; 8] {
25    // SAFETY: the block is 8 words of 4 bytes.
26    let words: [u32; 8] = unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast()) };
27    words.map(u32::from_le)
28}
29
30#[inline]
31fn store_block(block: [u32; 8], bytes: &mut [u8; BLOCK_BYTES]) {
32    // SAFETY: the block is 8 words of 4 bytes.
33    unsafe { std::ptr::write_unaligned(bytes.as_mut_ptr().cast(), block.map(u32::to_le)) }
34}
35
36/// The byte offset of the block of `hash` in `bitset`, which holds at least
37/// one block.
38#[inline]
39fn block_offset(bitset: &[u8], hash: u64) -> usize {
40    let num_blocks = bitset.len() / BLOCK_BYTES;
41    assert!(num_blocks > 0);
42    block_index(hash, num_blocks) * BLOCK_BYTES
43}
44
45#[inline]
46fn block_bytes(bitset: &[u8], hash: u64) -> &[u8; BLOCK_BYTES] {
47    let offset = block_offset(bitset, hash);
48    // SAFETY: `offset + BLOCK_BYTES <= bitset.len()`.
49    unsafe { &*(bitset.as_ptr().add(offset) as *const [u8; BLOCK_BYTES]) }
50}
51
52#[inline]
53fn block_bytes_mut(bitset: &mut [u8], hash: u64) -> &mut [u8; BLOCK_BYTES] {
54    let offset = block_offset(bitset, hash);
55    // SAFETY: `offset + BLOCK_BYTES <= bitset.len()`.
56    unsafe { &mut *(bitset.as_mut_ptr().add(offset) as *mut [u8; BLOCK_BYTES]) }
57}
58
59#[inline]
60fn block_contains(block: [u32; 8], mask: [u32; 8]) -> bool {
61    let mut found = true;
62    for i in 0..8 {
63        found &= block[i] & mask[i] != 0;
64    }
65    found
66}
67
68/// Whether `hash` is in the filter held by `bitset`, at least one block.
69pub fn is_in_set(bitset: &[u8], hash: u64) -> bool {
70    let block = load_block(block_bytes(bitset, hash));
71    block_contains(block, block_mask(hash))
72}
73
74/// Add `hash` to the filter held by `bitset`, at least one block.
75pub fn insert(bitset: &mut [u8], hash: u64) {
76    let bytes = block_bytes_mut(bitset, hash);
77    let mut block = load_block(bytes);
78    let mask = block_mask(hash);
79    for i in 0..8 {
80        block[i] |= mask[i];
81    }
82    store_block(block, bytes);
83}
84
85/// A split-block bloom filter over 64-bit hashes.
86#[derive(Clone)]
87pub struct SplitBlockBloom {
88    blocks: Vec<[u32; 8]>,
89}
90
91impl SplitBlockBloom {
92    fn num_blocks_for(num_keys: usize, bits_per_key: usize) -> usize {
93        num_keys
94            .saturating_mul(bits_per_key)
95            .div_ceil(BLOCK_BYTES * 8)
96            .max(1)
97            .next_power_of_two()
98    }
99
100    /// The bytes `with_capacity` allocates.
101    pub fn size_for(num_keys: usize, bits_per_key: usize) -> usize {
102        Self::num_blocks_for(num_keys, bits_per_key).saturating_mul(BLOCK_BYTES)
103    }
104
105    /// A filter sized for `num_keys` keys at `bits_per_key` bits each, rounded
106    /// up to a power of two blocks.
107    pub fn with_capacity(num_keys: usize, bits_per_key: usize) -> Self {
108        Self {
109            blocks: vec![[0; 8]; Self::num_blocks_for(num_keys, bits_per_key)],
110        }
111    }
112
113    pub fn size_bytes(&self) -> usize {
114        self.blocks.len() * BLOCK_BYTES
115    }
116
117    /// The number of bits the filter holds.
118    pub fn num_bits(&self) -> usize {
119        self.size_bytes() * 8
120    }
121
122    #[inline]
123    pub fn insert(&mut self, hash: u64) {
124        let b = block_index(hash, self.blocks.len());
125        // SAFETY: `b` is below the number of blocks.
126        let block = unsafe { self.blocks.get_unchecked_mut(b) };
127        let mask = block_mask(hash);
128        for i in 0..8 {
129            block[i] |= mask[i];
130        }
131    }
132
133    #[inline]
134    pub fn contains(&self, hash: u64) -> bool {
135        let b = block_index(hash, self.blocks.len());
136        // SAFETY: `b` is below the number of blocks.
137        let block = unsafe { *self.blocks.get_unchecked(b) };
138        block_contains(block, block_mask(hash))
139    }
140
141    /// Add every key of `other`, a filter of the same size.
142    pub fn union_with(&mut self, other: &Self) {
143        assert_eq!(self.blocks.len(), other.blocks.len());
144        for (a, b) in self.blocks.iter_mut().zip(&other.blocks) {
145            for i in 0..8 {
146                a[i] |= b[i];
147            }
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn struct_matches_bitset() {
158        let mut bloom = SplitBlockBloom::with_capacity(1000, 8);
159        let mut bitset = vec![0u8; bloom.size_bytes()];
160        let hashes: Vec<u64> = (0..1000u64)
161            .map(|i| i.wrapping_mul(0x9E3779B97F4A7C15))
162            .collect();
163        for &h in &hashes {
164            bloom.insert(h);
165            insert(&mut bitset, h);
166        }
167        for h in hashes
168            .iter()
169            .copied()
170            .chain((1000..2000u64).map(|i| i.wrapping_mul(0x9E3779B97F4A7C15)))
171        {
172            assert_eq!(bloom.contains(h), is_in_set(&bitset, h));
173        }
174        assert!(hashes.iter().all(|&h| bloom.contains(h)));
175    }
176
177    #[test]
178    #[should_panic]
179    fn is_in_set_needs_a_block() {
180        is_in_set(&[], 0);
181    }
182
183    #[test]
184    #[should_panic]
185    fn insert_needs_a_block() {
186        insert(&mut [0u8; 31], 0);
187    }
188
189    #[test]
190    fn union() {
191        let mut a = SplitBlockBloom::with_capacity(100, 8);
192        let mut b = a.clone();
193        a.insert(1);
194        b.insert(2);
195        a.union_with(&b);
196        assert!(a.contains(1) && a.contains(2));
197    }
198}