Skip to content

Commit e165457

Browse files
committed
introduce a more generic extend function
1 parent 52b25c6 commit e165457

3 files changed

Lines changed: 294 additions & 80 deletions

File tree

crates/geo_filters/evaluation/performance.rs

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ fn criterion_benchmark(c: &mut Criterion) {
133133
}
134134

135135
// Compare building a diff filter from a precomputed slice of hashes one by one
136-
// (`push_hash`) versus in a single batch (`from_hashes`). The hashes are precomputed so that
137-
// only the construction cost is measured.
136+
// (`push_hash`) versus in a single batch (`extend_by_hashes` on an empty filter). The hashes
137+
// are precomputed so that only the construction cost is measured.
138138
for size in [1000usize, 10000, 100000, 1000000] {
139139
let mut group = c.benchmark_group(format!("construct:{size}"));
140140
let build_hasher = UnstableDefaultBuildHasher::default();
@@ -149,12 +149,11 @@ fn criterion_benchmark(c: &mut Criterion) {
149149
black_box(&gc);
150150
})
151151
});
152-
group.bench_function("geo_diff_count_7_from_hashes", |b| {
152+
group.bench_function("geo_diff_count_7_extend", |b| {
153153
b.iter(|| {
154-
black_box(GeoDiffCount7::from_hashes(
155-
Default::default(),
156-
hashes.iter().copied(),
157-
));
154+
let mut gc = GeoDiffCount7::default();
155+
gc.extend_by_hashes(hashes.iter().copied());
156+
black_box(&gc);
158157
})
159158
});
160159
group.bench_function("geo_diff_count_13_push", |b| {
@@ -166,15 +165,60 @@ fn criterion_benchmark(c: &mut Criterion) {
166165
black_box(&gc);
167166
})
168167
});
169-
group.bench_function("geo_diff_count_13_from_hashes", |b| {
168+
group.bench_function("geo_diff_count_13_extend", |b| {
170169
b.iter(|| {
171-
black_box(GeoDiffCount13::from_hashes(
172-
Default::default(),
173-
hashes.iter().copied(),
174-
));
170+
let mut gc = GeoDiffCount13::default();
171+
gc.extend_by_hashes(hashes.iter().copied());
172+
black_box(&gc);
175173
})
176174
});
177175
}
176+
177+
// Insert a batch of hashes into an existing filter: one-by-one `push_hash` vs the batched
178+
// `extend_by_hashes`. The existing filter is cloned in the (untimed) batched setup so that only
179+
// the insertion cost is measured, not the clone.
180+
for (existing_n, batch_n) in [
181+
(10000usize, 10000usize),
182+
(100000, 10000),
183+
(100000, 100000),
184+
(1000, 100000),
185+
] {
186+
let mut group = c.benchmark_group(format!("insert_batch:{existing_n}+{batch_n}"));
187+
let build_hasher = UnstableDefaultBuildHasher::default();
188+
let batch: Vec<u64> = (existing_n..existing_n + batch_n)
189+
.map(|i| build_hasher.hash_one(i))
190+
.collect();
191+
let base = {
192+
let mut gc = GeoDiffCount13::default();
193+
for i in 0..existing_n {
194+
gc.push_hash(build_hasher.hash_one(i));
195+
}
196+
gc
197+
};
198+
199+
group.bench_function("push", |b| {
200+
b.iter_batched(
201+
|| base.clone(),
202+
|mut gc| {
203+
for &hash in &batch {
204+
gc.push_hash(hash);
205+
}
206+
gc
207+
},
208+
criterion::BatchSize::SmallInput,
209+
)
210+
});
211+
group.bench_function("extend_by_hashes", |b| {
212+
b.iter_batched(
213+
|| base.clone(),
214+
|mut gc| {
215+
gc.extend_by_hashes(batch.iter().copied());
216+
gc
217+
},
218+
criterion::BatchSize::SmallInput,
219+
)
220+
});
221+
}
178222
}
179223

180224
criterion_group!(benches, criterion_benchmark);

crates/geo_filters/src/config/bitchunks.rs

Lines changed: 76 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,
@@ -78,6 +82,39 @@ impl<I: Iterator<Item = BitChunk>, J: Iterator<Item = usize>> Iterator for BitCh
7882
}
7983
}
8084

85+
/// Turns a descending stream of one-bit positions into a descending `BitChunk` stream containing
86+
/// each position's parity. Unlike [`iter_bit_chunks`], positions may repeat: a position occurring
87+
/// an even number of times cancels out (xor), and a block that cancels out entirely is skipped.
88+
pub(crate) fn parity_bit_positions(
89+
positions: impl Iterator<Item = usize>,
90+
) -> impl Iterator<Item = BitChunk> {
91+
ParityBitPositions(positions.peekable())
92+
}
93+
94+
struct ParityBitPositions<I: Iterator<Item = usize>>(Peekable<I>);
95+
96+
impl<I: Iterator<Item = usize>> Iterator for ParityBitPositions<I> {
97+
type Item = BitChunk;
98+
99+
fn next(&mut self) -> Option<Self::Item> {
100+
loop {
101+
let (index, bit) = self.0.next()?.into_index_and_bit();
102+
let mut block: u64 = bit.into_block();
103+
while let Some(&other) = self.0.peek() {
104+
if other.into_index() != index {
105+
break;
106+
}
107+
block ^= other.into_bit().into_block();
108+
self.0.next();
109+
}
110+
// The block is empty only if its positions cancelled out entirely; skip it.
111+
if block != 0 {
112+
return Some(BitChunk::new(index, block));
113+
}
114+
}
115+
}
116+
}
117+
81118
/// Combine-s two `BitChunk` iterators using the given operator. Both iterators have to output
82119
/// `BitChunk`s from most to least significant and must not report duplicates!
83120
struct BinOpBitChunksIterator<
@@ -314,7 +351,7 @@ impl<T: IsBucketType, I: Iterator<Item = BitChunk>> Iterator for BitChunksOnes<T
314351
mod tests {
315352
use itertools::Itertools;
316353

317-
use super::{iter_ones, BitChunk};
354+
use super::{iter_bit_chunks, iter_ones, parity_bit_positions, BitChunk};
318355

319356
#[test]
320357
fn test_iter_ones() {
@@ -338,4 +375,40 @@ mod tests {
338375
iter_ones::<usize, _>(chunks.into_iter().peekable()).collect_vec()
339376
);
340377
}
378+
379+
#[test]
380+
fn test_iter_bit_chunks() {
381+
// Distinct leading bits merge within a block (via or) and merge with the trailing block at
382+
// the boundary index.
383+
let chunks = iter_bit_chunks(
384+
vec![70, 67, 5].into_iter(),
385+
vec![BitChunk::new(0, 1 << 2)].into_iter(),
386+
)
387+
.collect_vec();
388+
assert_eq!(
389+
chunks,
390+
vec![
391+
BitChunk::new(1, (1 << 6) | (1 << 3)), // 70, 67
392+
BitChunk::new(0, (1 << 5) | (1 << 2)), // 5 (leading) and bit 2 (trailing)
393+
]
394+
);
395+
}
396+
397+
#[test]
398+
fn test_parity_bit_positions() {
399+
// Equal positions cancel (xor): 70 twice cancels, 67 stays; 5 three times stays, 3 once.
400+
let chunks = parity_bit_positions(vec![70, 70, 67, 5, 5, 5, 3].into_iter()).collect_vec();
401+
assert_eq!(
402+
chunks,
403+
vec![
404+
BitChunk::new(1, 1 << 3), // 67
405+
BitChunk::new(0, (1 << 5) | (1 << 3)), // 5, 3
406+
]
407+
);
408+
409+
// A block whose positions cancel out entirely is skipped.
410+
assert!(parity_bit_positions(vec![5, 5].into_iter())
411+
.collect_vec()
412+
.is_empty());
413+
}
341414
}

0 commit comments

Comments
 (0)