Skip to content

Commit 4ea41c5

Browse files
authored
Merge pull request #131 from github/aneubeck/fastgeo
Introduce an optimized extend_by_hashes function
2 parents fa1c18e + f278b8b commit 4ea41c5

5 files changed

Lines changed: 507 additions & 10 deletions

File tree

‎crates/geo_filters/evaluation/performance.rs‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
use std::hash::BuildHasher;
12
use std::hint::black_box;
23

34
use criterion::{criterion_group, criterion_main, Criterion};
45
use geo_filters::build_hasher::UnstableDefaultBuildHasher;
56
use geo_filters::config::VariableConfig;
6-
use geo_filters::diff_count::{GeoDiffCount, GeoDiffCount13};
7+
use geo_filters::diff_count::{
8+
GeoDiffConfig13, GeoDiffCount, GeoDiffCount13, GeoDiffCount7, GeoDiffCountBuilder,
9+
};
710
use geo_filters::distinct_count::GeoDistinctCount13;
811
use geo_filters::evaluation::hll::Hll14;
912
use geo_filters::Count;
@@ -130,6 +133,71 @@ fn criterion_benchmark(c: &mut Criterion) {
130133
})
131134
});
132135
}
136+
137+
// Compare building a diff filter from a precomputed slice of hashes one by one (`push_hash`)
138+
// versus via the incremental `GeoDiffCountBuilder` (per-hash, and the batched
139+
// `extend_by_hashes`). The hashes are precomputed so that only construction cost is measured.
140+
for size in [1000usize, 10000, 100000, 1000000] {
141+
let mut group = c.benchmark_group(format!("construct:{size}"));
142+
let build_hasher = UnstableDefaultBuildHasher::default();
143+
let hashes: Vec<u64> = (0..size).map(|i| build_hasher.hash_one(i)).collect();
144+
145+
group.bench_function("geo_diff_count_7_push", |b| {
146+
b.iter(|| {
147+
let mut gc = GeoDiffCount7::default();
148+
for &hash in &hashes {
149+
gc.push_hash(hash);
150+
}
151+
black_box(&gc);
152+
})
153+
});
154+
group.bench_function("geo_diff_count_13_push", |b| {
155+
b.iter(|| {
156+
let mut gc = GeoDiffCount13::default();
157+
for &hash in &hashes {
158+
gc.push_hash(hash);
159+
}
160+
black_box(&gc);
161+
})
162+
});
163+
group.bench_function("geo_diff_count_13_builder_extend", |b| {
164+
b.iter(|| {
165+
let mut builder = GeoDiffCountBuilder::with_capacity(
166+
GeoDiffConfig13::<UnstableDefaultBuildHasher>::default(),
167+
0,
168+
);
169+
builder.extend_by_hashes(hashes.iter().copied());
170+
black_box(builder.build());
171+
})
172+
});
173+
group.bench_function("geo_diff_count_13_builder", |b| {
174+
b.iter(|| {
175+
let mut builder = GeoDiffCountBuilder::with_capacity(
176+
GeoDiffConfig13::<UnstableDefaultBuildHasher>::default(),
177+
size,
178+
);
179+
for &hash in &hashes {
180+
builder.push_hash(hash);
181+
}
182+
black_box(builder.build());
183+
})
184+
});
185+
// Reserve nothing so the split starts at 0 and every bucket initially lands in `numbers`,
186+
// forcing the buffer to fill and compact (lazily flush) repeatedly as the split ramps up.
187+
// This isolates the cost of the lazy-flush path versus a well-positioned builder.
188+
group.bench_function("geo_diff_count_13_builder_unreserved", |b| {
189+
b.iter(|| {
190+
let mut builder = GeoDiffCountBuilder::with_capacity(
191+
GeoDiffConfig13::<UnstableDefaultBuildHasher>::default(),
192+
0,
193+
);
194+
for &hash in &hashes {
195+
builder.push_hash(hash);
196+
}
197+
black_box(builder.build());
198+
})
199+
});
200+
}
133201
}
134202

135203
criterion_group!(benches, criterion_benchmark);

‎crates/geo_filters/src/config/bitchunks.rs‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ impl BitChunk {
2020
}
2121
}
2222

23+
/// Merges a descending stream of distinct one-bit positions (`leading`) with a descending stream
24+
/// of `BitChunk`s (`trailing`) into a single descending `BitChunk` stream. All leading positions
25+
/// must be more significant than all trailing bits, except that the least-significant leading block
26+
/// may overlap the most-significant trailing block (the two are or-ed). Leading positions must be
27+
/// distinct.
2328
pub(crate) fn iter_bit_chunks(
2429
leading: impl Iterator<Item = usize>,
2530
trailing: impl Iterator<Item = BitChunk>,
@@ -55,8 +60,7 @@ impl<I: Iterator<Item = BitChunk>, J: Iterator<Item = usize>> Iterator for BitCh
5560
_ => break,
5661
}
5762
}
58-
// All leading bits were consumed, test whether it can be merged with
59-
// trailing bits.
63+
// All leading bits were consumed, test whether it can be merged with trailing bits.
6064
match self.trailing.peek() {
6165
Some(BitChunk {
6266
index: other_index,
@@ -314,7 +318,7 @@ impl<T: IsBucketType, I: Iterator<Item = BitChunk>> Iterator for BitChunksOnes<T
314318
mod tests {
315319
use itertools::Itertools;
316320

317-
use super::{iter_ones, BitChunk};
321+
use super::{iter_bit_chunks, iter_ones, BitChunk};
318322

319323
#[test]
320324
fn test_iter_ones() {
@@ -338,4 +342,22 @@ mod tests {
338342
iter_ones::<usize, _>(chunks.into_iter().peekable()).collect_vec()
339343
);
340344
}
345+
346+
#[test]
347+
fn test_iter_bit_chunks() {
348+
// Distinct leading bits merge within a block (via or) and merge with the trailing block at
349+
// the boundary index.
350+
let chunks = iter_bit_chunks(
351+
vec![70, 67, 5].into_iter(),
352+
vec![BitChunk::new(0, 1 << 2)].into_iter(),
353+
)
354+
.collect_vec();
355+
assert_eq!(
356+
chunks,
357+
vec![
358+
BitChunk::new(1, (1 << 6) | (1 << 3)), // 70, 67
359+
BitChunk::new(0, (1 << 5) | (1 << 2)), // 5 (leading) and bit 2 (trailing)
360+
]
361+
);
362+
}
341363
}

‎crates/geo_filters/src/config/lookup.rs‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ use crate::config::phi_f64;
22

33
pub(crate) struct HashToBucketLookup {
44
b: usize,
5-
buckets: Vec<(usize, usize)>,
5+
buckets: Vec<(u32, u32)>,
66
}
77

88
impl HashToBucketLookup {
99
pub(crate) fn new(b: usize) -> Self {
10-
let mut buckets = vec![(0, 0); 2 << b];
10+
let mut buckets = vec![(0u32, 0u32); 2 << b];
1111
let mut last_filled_bucket = buckets.len();
1212
let phi = phi_f64(b);
1313
for bucket in 0..(1 << b) {
1414
let lower_bucket_limit = phi.powf((bucket + 1) as f64);
15+
// `lower_hash_limit` is a 32-bit hash threshold: `lower_bucket_limit` lies in
16+
// `[0.5, 1)`, so this value is always in `[0, 2^32)` and fits losslessly into a `u32`.
1517
let lower_hash_limit = ((lower_bucket_limit - 0.5) * 2.0f64.powf(33.0)) as usize;
1618
let lower_hash_bucket = lower_hash_limit >> (32 - b - 1);
1719
assert!(lower_hash_bucket < last_filled_bucket);
1820
while last_filled_bucket > lower_hash_bucket {
1921
last_filled_bucket -= 1;
20-
buckets[last_filled_bucket] = (bucket, lower_hash_limit);
22+
buckets[last_filled_bucket] = (bucket as u32, lower_hash_limit as u32);
2123
}
2224
}
2325
assert_eq!(last_filled_bucket, 0);
@@ -38,8 +40,12 @@ impl HashToBucketLookup {
3840
} & 0xFFFFFFFF) as usize;
3941
// From those, the first B bits determine the bucket index in our lookup table.
4042
let idx = hash >> (32 - self.b - 1);
41-
let offset = (hash < self.buckets[idx].1) as usize;
42-
offset + self.buckets[idx].0 + (1 << self.b) * levels
43+
// SAFETY: `hash` was masked to 32 bits, so `idx = hash >> (31 - b)` holds at most `b + 1`
44+
// significant bits and is therefore always `< 2^(b+1) == 2 << b == self.buckets.len()`.
45+
debug_assert!(idx < self.buckets.len());
46+
let (base, threshold) = *unsafe { self.buckets.get_unchecked(idx) };
47+
let offset = (hash < threshold as usize) as usize;
48+
offset + base as usize + (1 << self.b) * levels
4349
}
4450
}
4551

0 commit comments

Comments
 (0)