polars_utils/
bloom_filter.rs1use std::mem::ManuallyDrop;
6use std::sync::atomic::{AtomicU64, Ordering};
7
8use bytemuck::Zeroable;
9use bytemuck::allocation::zeroed_vec;
10
11use crate::mem::prefetch::prefetch_l1;
12
13const SALT: [u32; 8] = [
14 1203114875, 1150766481, 2284105051, 2729912477, 1884591559, 770785867, 2667333959, 1550580529,
15];
16
17const BLOCK_BYTES: usize = 32;
18
19#[inline]
21fn block_index(hash: u64, num_blocks: usize) -> usize {
22 (((hash >> 32) * num_blocks as u64) >> 32) as usize
23}
24
25#[inline]
26fn block_mask(hash: u64) -> [u32; 8] {
27 let key = hash as u32;
28 std::array::from_fn(|i| 1u32 << (key.wrapping_mul(SALT[i]) >> 27))
29}
30
31#[inline]
32fn load_block(bytes: &[u8; BLOCK_BYTES]) -> [u32; 8] {
33 let words: [u32; 8] = unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast()) };
35 words.map(u32::from_le)
36}
37
38#[inline]
39fn store_block(block: [u32; 8], bytes: &mut [u8; BLOCK_BYTES]) {
40 unsafe { std::ptr::write_unaligned(bytes.as_mut_ptr().cast(), block.map(u32::to_le)) }
42}
43
44#[inline]
47fn block_offset(bitset: &[u8], hash: u64) -> usize {
48 let num_blocks = bitset.len() / BLOCK_BYTES;
49 assert!(num_blocks > 0);
50 block_index(hash, num_blocks) * BLOCK_BYTES
51}
52
53#[inline]
54fn block_bytes(bitset: &[u8], hash: u64) -> &[u8; BLOCK_BYTES] {
55 let offset = block_offset(bitset, hash);
56 unsafe { &*(bitset.as_ptr().add(offset) as *const [u8; BLOCK_BYTES]) }
58}
59
60#[inline]
61fn block_bytes_mut(bitset: &mut [u8], hash: u64) -> &mut [u8; BLOCK_BYTES] {
62 let offset = block_offset(bitset, hash);
63 unsafe { &mut *(bitset.as_mut_ptr().add(offset) as *mut [u8; BLOCK_BYTES]) }
65}
66
67#[inline]
68fn block_contains(block: [u32; 8], mask: [u32; 8]) -> bool {
69 let mut found = true;
70 for i in 0..8 {
71 found &= block[i] & mask[i] != 0;
72 }
73 found
74}
75
76pub fn is_in_set(bitset: &[u8], hash: u64) -> bool {
78 let block = load_block(block_bytes(bitset, hash));
79 block_contains(block, block_mask(hash))
80}
81
82pub fn insert(bitset: &mut [u8], hash: u64) {
84 let bytes = block_bytes_mut(bitset, hash);
85 let mut block = load_block(bytes);
86 let mask = block_mask(hash);
87 for i in 0..8 {
88 block[i] |= mask[i];
89 }
90 store_block(block, bytes);
91}
92
93#[derive(Clone, Copy, Zeroable)]
96#[repr(C, align(32))]
97struct Block([u32; 8]);
98
99#[derive(Clone)]
101pub struct SplitBlockBloom {
102 blocks: Vec<Block>,
103}
104
105impl SplitBlockBloom {
106 fn num_blocks_for(num_keys: usize, bits_per_key: usize) -> usize {
107 num_keys
108 .saturating_mul(bits_per_key)
109 .div_ceil(BLOCK_BYTES * 8)
110 .max(1)
111 .next_power_of_two()
112 }
113
114 pub fn size_for(num_keys: usize, bits_per_key: usize) -> usize {
116 Self::num_blocks_for(num_keys, bits_per_key).saturating_mul(BLOCK_BYTES)
117 }
118
119 pub fn with_capacity(num_keys: usize, bits_per_key: usize) -> Self {
123 Self {
124 blocks: zeroed_vec(Self::num_blocks_for(num_keys, bits_per_key)),
125 }
126 }
127
128 pub fn size_bytes(&self) -> usize {
129 self.blocks.len() * BLOCK_BYTES
130 }
131
132 pub fn num_bits(&self) -> usize {
134 self.size_bytes() * 8
135 }
136
137 pub fn insert_many(&mut self, hashes: &[u64]) {
138 let blocks = &mut self.blocks;
139 for_each_block_mask(hashes, blocks.as_ptr().cast(), blocks.len(), |b, mask| {
140 let block = unsafe { &mut blocks.get_unchecked_mut(b).0 };
142 for i in 0..8 {
143 block[i] |= mask[i];
144 }
145 });
146 }
147
148 #[inline]
149 pub fn contains(&self, hash: u64) -> bool {
150 let b = block_index(hash, self.blocks.len());
151 let block = unsafe { self.blocks.get_unchecked(b).0 };
153 block_contains(block, block_mask(hash))
154 }
155}
156
157const INSERT_GROUP: usize = 64;
159const MIN_PREFETCH_BLOCKS: usize = 1 << 15;
161
162#[inline(always)]
167fn for_each_block_mask(
168 hashes: &[u64],
169 blocks: *const u8,
170 num_blocks: usize,
171 mut f: impl FnMut(usize, [u32; 8]),
172) {
173 let prefetch = num_blocks >= MIN_PREFETCH_BLOCKS;
174 let mut masks = [[0u32; 8]; INSERT_GROUP];
175 let mut idxs = [0usize; INSERT_GROUP];
176 for group in hashes.chunks(INSERT_GROUP) {
177 for ((hash, mask), b) in group.iter().zip(&mut masks).zip(&mut idxs) {
178 *mask = block_mask(*hash);
179 *b = block_index(*hash, num_blocks);
180 }
181 let idxs = &idxs[..group.len()];
182 if prefetch {
183 for b in idxs {
186 prefetch_l1(blocks.wrapping_add(b * BLOCK_BYTES));
187 }
188 }
189 for (mask, b) in masks.iter().zip(idxs) {
190 f(*b, *mask);
191 }
192 }
193}
194
195#[repr(C, align(32))]
198struct AtomicBlock([AtomicU64; 4]);
199
200const _: () = assert!(
201 size_of::<AtomicBlock>() == size_of::<Block>()
202 && align_of::<AtomicBlock>() == align_of::<Block>()
203);
204
205pub struct AtomicSplitBlockBloom {
209 blocks: Vec<AtomicBlock>,
210}
211
212impl From<SplitBlockBloom> for AtomicSplitBlockBloom {
213 fn from(bloom: SplitBlockBloom) -> Self {
214 Self {
217 blocks: unsafe { cast_vec(bloom.blocks) },
218 }
219 }
220}
221
222impl AtomicSplitBlockBloom {
223 pub fn insert_many(&self, hashes: &[u64]) {
224 let blocks = &self.blocks;
225 for_each_block_mask(hashes, blocks.as_ptr().cast(), blocks.len(), |b, mask| {
226 let block = unsafe { &blocks.get_unchecked(b).0 };
228 let mask: [u64; 4] = bytemuck::cast(mask);
229 let words: [u64; 4] = std::array::from_fn(|i| block[i].load(Ordering::Relaxed));
230 let mut missing = 0;
231 for i in 0..4 {
232 missing |= mask[i] & !words[i];
233 }
234 if missing != 0 {
237 for i in 0..4 {
238 if mask[i] & !words[i] != 0 {
239 block[i].fetch_or(mask[i], Ordering::Relaxed);
240 }
241 }
242 }
243 });
244 }
245
246 pub fn into_inner(self) -> SplitBlockBloom {
248 SplitBlockBloom {
250 blocks: unsafe { cast_vec(self.blocks) },
251 }
252 }
253}
254
255unsafe fn cast_vec<T, U>(v: Vec<T>) -> Vec<U> {
261 let mut v = ManuallyDrop::new(v);
262 unsafe { Vec::from_raw_parts(v.as_mut_ptr().cast(), v.len(), v.capacity()) }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 #[should_panic]
271 fn is_in_set_needs_a_block() {
272 is_in_set(&[], 0);
273 }
274
275 #[test]
276 #[should_panic]
277 fn insert_needs_a_block() {
278 insert(&mut [0u8; 31], 0);
279 }
280
281 fn spread_hashes(range: std::ops::Range<u64>) -> Vec<u64> {
282 range.map(|i| i.wrapping_mul(0x9E3779B97F4A7C15)).collect()
283 }
284
285 fn words(bloom: &SplitBlockBloom) -> Vec<[u32; 8]> {
286 bloom.blocks.iter().map(|b| b.0).collect()
287 }
288
289 fn bitset_words(hashes: &[u64], size_bytes: usize) -> Vec<[u32; 8]> {
291 let mut bitset = vec![0u8; size_bytes];
292 hashes.iter().for_each(|h| insert(&mut bitset, *h));
293 bitset
294 .chunks_exact(BLOCK_BYTES)
295 .map(|b| load_block(b.try_into().unwrap()))
296 .collect()
297 }
298
299 #[test]
300 fn matches_bitset() {
301 for keys in [1000, 1 << 20] {
304 let hashes = spread_hashes(0..keys as u64);
305 let expected = |bloom: &SplitBlockBloom| bitset_words(&hashes, bloom.size_bytes());
306 for batch in [7, 100, keys] {
307 let mut plain = SplitBlockBloom::with_capacity(keys, 8);
308 hashes.chunks(batch).for_each(|c| plain.insert_many(c));
309 assert_eq!(plain.size_bytes(), SplitBlockBloom::size_for(keys, 8));
310 assert!(words(&plain) == expected(&plain));
311
312 let atomic = AtomicSplitBlockBloom::from(SplitBlockBloom::with_capacity(keys, 8));
313 hashes.chunks(batch).for_each(|c| atomic.insert_many(c));
314 let atomic = atomic.into_inner();
315 assert!(words(&atomic) == expected(&atomic));
316 }
317 }
318 }
319
320 #[test]
321 fn contains_like_bitset() {
322 let hashes = spread_hashes(0..1000);
323 let mut bloom = SplitBlockBloom::with_capacity(1000, 8);
324 bloom.insert_many(&hashes);
325 let mut bitset = vec![0u8; bloom.size_bytes()];
326 hashes.iter().for_each(|h| insert(&mut bitset, *h));
327 assert!(hashes.iter().all(|h| bloom.contains(*h)));
328 for h in spread_hashes(1000..2000) {
329 assert_eq!(bloom.contains(h), is_in_set(&bitset, h));
330 }
331 }
332
333 #[test]
334 fn insert_from_several_threads() {
335 let hashes = spread_hashes(0..100_000);
336 let shared = AtomicSplitBlockBloom::from(SplitBlockBloom::with_capacity(hashes.len(), 8));
337 std::thread::scope(|s| {
338 for part in hashes.chunks(10_000) {
339 let shared = &shared;
340 s.spawn(move || shared.insert_many(part));
341 }
342 });
343 let bloom = shared.into_inner();
344 assert!(words(&bloom) == bitset_words(&hashes, bloom.size_bytes()));
345 }
346}