Skip to content

Commit 5d0d3f4

Browse files
committed
add in-place filter for BufferMut + benchmarks
Signed-off-by: Connor Tsui <[email protected]>
1 parent 33e8cf0 commit 5d0d3f4

File tree

7 files changed

+590
-1
lines changed

7 files changed

+590
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vortex-compute/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,10 @@ comparison = []
4040
filter = []
4141
logical = []
4242
mask = []
43+
44+
[dev-dependencies]
45+
divan = { workspace = true }
46+
47+
[[bench]]
48+
name = "filter_buffer"
49+
harness = false
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! In-place filter (ipf) benchmark for `BufferMut`.
5+
6+
use divan::Bencher;
7+
use vortex_buffer::BufferMut;
8+
use vortex_compute::in_place_filter::InPlaceFilter;
9+
use vortex_mask::Mask;
10+
11+
fn main() {
12+
divan::main();
13+
}
14+
15+
// Buffer size to test - focusing on 1024 for now
16+
const BUFFER_SIZE: usize = 1024;
17+
18+
// Pattern types for testing.
19+
#[derive(Copy, Clone, Debug)]
20+
enum Pattern {
21+
Random,
22+
Contiguous,
23+
Alternating,
24+
}
25+
26+
impl std::fmt::Display for Pattern {
27+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28+
match self {
29+
Pattern::Random => write!(f, "random"),
30+
Pattern::Contiguous => write!(f, "contiguous"),
31+
Pattern::Alternating => write!(f, "alternating"),
32+
}
33+
}
34+
}
35+
36+
/// Creates a test buffer filled with sequential values.
37+
fn create_test_buffer<T>(size: usize) -> BufferMut<T>
38+
where
39+
T: Copy + Default + From<u8>,
40+
{
41+
let mut buffer = BufferMut::with_capacity(size);
42+
for i in 0..size {
43+
#[expect(clippy::cast_possible_truncation)]
44+
buffer.push(T::from((i % 256) as u8));
45+
}
46+
buffer
47+
}
48+
49+
/// Generates a mask with the specified selectivity and pattern.
50+
fn generate_mask(len: usize, selectivity: f64, pattern: Pattern) -> Mask {
51+
#[expect(clippy::cast_possible_truncation)]
52+
#[expect(clippy::cast_sign_loss)]
53+
let num_selected = ((len as f64) * selectivity).round() as usize;
54+
55+
let selection = match pattern {
56+
Pattern::Random => {
57+
// Random selection - distribute selected elements randomly.
58+
// Use a deterministic pattern for reproducibility.
59+
let mut selection = vec![false; len];
60+
let mut indices: Vec<usize> = (0..len).collect();
61+
62+
// Simple deterministic shuffle.
63+
for i in (1..len).rev() {
64+
let j = (i * 7 + 13) % (i + 1);
65+
indices.swap(i, j);
66+
}
67+
68+
for i in 0..num_selected.min(len) {
69+
selection[indices[i]] = true;
70+
}
71+
selection
72+
}
73+
Pattern::Contiguous => {
74+
// One contiguous block in the middle.
75+
let mut selection = vec![false; len];
76+
let start = (len.saturating_sub(num_selected)) / 2;
77+
for i in start..(start + num_selected).min(len) {
78+
selection[i] = true;
79+
}
80+
selection
81+
}
82+
Pattern::Alternating => {
83+
// Select every nth element to achieve desired selectivity.
84+
let mut selection = vec![false; len];
85+
if num_selected > 0 {
86+
let step = len.max(1) / num_selected.max(1);
87+
let step = step.max(1);
88+
for i in (0..len).step_by(step).take(num_selected) {
89+
selection[i] = true;
90+
}
91+
}
92+
selection
93+
}
94+
};
95+
96+
Mask::from_iter(selection)
97+
}
98+
99+
// ===== PRIMARY BENCHMARK: Full Selectivity Spectrum =====
100+
// This shows performance across the entire selectivity range
101+
// with extra detail around the 80% threshold.
102+
103+
// Macro to generate a type/size benchmark module with all selectivity benchmarks.
104+
macro_rules! type_size_bench_group {
105+
($mod_name:ident, $type:ty) => {
106+
#[divan::bench_group]
107+
mod $mod_name {
108+
use super::*;
109+
type T = $type;
110+
const SIZE: usize = BUFFER_SIZE;
111+
112+
// Inner macro for generating individual selectivity benchmarks.
113+
macro_rules! selectivity_bench {
114+
($name: ident,$selectivity: expr) => {
115+
#[divan::bench(sample_count = 1000)]
116+
fn $name(bencher: Bencher) {
117+
bencher
118+
.with_inputs(|| {
119+
let buffer = create_test_buffer::<T>(SIZE);
120+
let mask = generate_mask(SIZE, $selectivity, Pattern::Random);
121+
(buffer, mask)
122+
})
123+
.bench_values(|(mut buffer, mask)| {
124+
buffer.in_place_filter(&mask);
125+
divan::black_box(buffer);
126+
});
127+
}
128+
};
129+
}
130+
131+
// Generate benchmarks for each selectivity level.
132+
selectivity_bench!(sel_01_percent, 0.01);
133+
selectivity_bench!(sel_25_percent, 0.25);
134+
selectivity_bench!(sel_50_percent, 0.50);
135+
selectivity_bench!(sel_75_percent, 0.75);
136+
selectivity_bench!(sel_78_percent, 0.78);
137+
selectivity_bench!(sel_79_percent, 0.79);
138+
selectivity_bench!(sel_80_percent, 0.80);
139+
selectivity_bench!(sel_81_percent, 0.81);
140+
selectivity_bench!(sel_82_percent, 0.82);
141+
selectivity_bench!(sel_85_percent, 0.85);
142+
selectivity_bench!(sel_99_percent, 0.99);
143+
}
144+
};
145+
}
146+
147+
// Generate benchmark modules for each type.
148+
type_size_bench_group!(u8_1024, u8);
149+
type_size_bench_group!(u32_1024, u32);
150+
type_size_bench_group!(u64_1024, u64);
151+
152+
// ===== PATTERN COMPARISON AT THRESHOLD =====
153+
// Test different patterns but ONLY at the 80% threshold where the algorithm choice matters most.
154+
// This tests whether certain patterns perform better with the index-based vs slice-based approach.
155+
156+
#[divan::bench_group]
157+
mod u32_1024_patterns {
158+
use super::*;
159+
type T = u32;
160+
const SIZE: usize = BUFFER_SIZE;
161+
const SELECTIVITY: f64 = 0.80;
162+
163+
#[divan::bench(sample_count = 1000)]
164+
fn random(bencher: Bencher) {
165+
bencher
166+
.with_inputs(|| {
167+
let buffer = create_test_buffer::<T>(SIZE);
168+
let mask = generate_mask(SIZE, SELECTIVITY, Pattern::Random);
169+
(buffer, mask)
170+
})
171+
.bench_values(|(mut buffer, mask)| {
172+
buffer.in_place_filter(&mask);
173+
divan::black_box(buffer);
174+
});
175+
}
176+
177+
#[divan::bench(sample_count = 1000)]
178+
fn contiguous(bencher: Bencher) {
179+
bencher
180+
.with_inputs(|| {
181+
let buffer = create_test_buffer::<T>(SIZE);
182+
let mask = generate_mask(SIZE, SELECTIVITY, Pattern::Contiguous);
183+
(buffer, mask)
184+
})
185+
.bench_values(|(mut buffer, mask)| {
186+
buffer.in_place_filter(&mask);
187+
divan::black_box(buffer);
188+
});
189+
}
190+
191+
#[divan::bench(sample_count = 1000)]
192+
fn alternating(bencher: Bencher) {
193+
bencher
194+
.with_inputs(|| {
195+
let buffer = create_test_buffer::<T>(SIZE);
196+
let mask = generate_mask(SIZE, SELECTIVITY, Pattern::Alternating);
197+
(buffer, mask)
198+
})
199+
.bench_values(|(mut buffer, mask)| {
200+
buffer.in_place_filter(&mask);
201+
divan::black_box(buffer);
202+
});
203+
}
204+
}
205+
206+
// ===== LARGE ELEMENT BENCHMARKS =====
207+
// Test with larger element sizes at the critical threshold range to understand
208+
// how memcpy performance affects the algorithms.
209+
210+
#[derive(Copy, Clone, Default)]
211+
#[allow(dead_code)]
212+
struct LargeElement([u8; 32]);
213+
214+
impl From<u8> for LargeElement {
215+
fn from(value: u8) -> Self {
216+
LargeElement([value; 32])
217+
}
218+
}
219+
220+
#[divan::bench_group]
221+
mod large_elem_1024 {
222+
use super::*;
223+
type T = LargeElement;
224+
const SIZE: usize = BUFFER_SIZE;
225+
226+
#[divan::bench(sample_count = 1000)]
227+
fn sel_50_percent(bencher: Bencher) {
228+
bencher
229+
.with_inputs(|| {
230+
let buffer = create_test_buffer::<T>(SIZE);
231+
let mask = generate_mask(SIZE, 0.50, Pattern::Random);
232+
(buffer, mask)
233+
})
234+
.bench_values(|(mut buffer, mask)| {
235+
buffer.in_place_filter(&mask);
236+
divan::black_box(buffer);
237+
});
238+
}
239+
240+
#[divan::bench(sample_count = 1000)]
241+
fn sel_75_percent(bencher: Bencher) {
242+
bencher
243+
.with_inputs(|| {
244+
let buffer = create_test_buffer::<T>(SIZE);
245+
let mask = generate_mask(SIZE, 0.75, Pattern::Random);
246+
(buffer, mask)
247+
})
248+
.bench_values(|(mut buffer, mask)| {
249+
buffer.in_place_filter(&mask);
250+
divan::black_box(buffer);
251+
});
252+
}
253+
254+
#[divan::bench(sample_count = 1000)]
255+
fn sel_79_percent(bencher: Bencher) {
256+
bencher
257+
.with_inputs(|| {
258+
let buffer = create_test_buffer::<T>(SIZE);
259+
let mask = generate_mask(SIZE, 0.79, Pattern::Random);
260+
(buffer, mask)
261+
})
262+
.bench_values(|(mut buffer, mask)| {
263+
buffer.in_place_filter(&mask);
264+
divan::black_box(buffer);
265+
});
266+
}
267+
268+
#[divan::bench(sample_count = 1000)]
269+
fn sel_80_percent(bencher: Bencher) {
270+
bencher
271+
.with_inputs(|| {
272+
let buffer = create_test_buffer::<T>(SIZE);
273+
let mask = generate_mask(SIZE, 0.80, Pattern::Random);
274+
(buffer, mask)
275+
})
276+
.bench_values(|(mut buffer, mask)| {
277+
buffer.in_place_filter(&mask);
278+
divan::black_box(buffer);
279+
});
280+
}
281+
282+
#[divan::bench(sample_count = 1000)]
283+
fn sel_81_percent(bencher: Bencher) {
284+
bencher
285+
.with_inputs(|| {
286+
let buffer = create_test_buffer::<T>(SIZE);
287+
let mask = generate_mask(SIZE, 0.81, Pattern::Random);
288+
(buffer, mask)
289+
})
290+
.bench_values(|(mut buffer, mask)| {
291+
buffer.in_place_filter(&mask);
292+
divan::black_box(buffer);
293+
});
294+
}
295+
296+
#[divan::bench]
297+
fn sel_85_percent(bencher: Bencher) {
298+
bencher
299+
.with_inputs(|| {
300+
let buffer = create_test_buffer::<T>(SIZE);
301+
let mask = generate_mask(SIZE, 0.85, Pattern::Random);
302+
(buffer, mask)
303+
})
304+
.bench_values(|(mut buffer, mask)| {
305+
buffer.in_place_filter(&mask);
306+
divan::black_box(buffer);
307+
});
308+
}
309+
310+
#[divan::bench]
311+
fn sel_90_percent(bencher: Bencher) {
312+
bencher
313+
.with_inputs(|| {
314+
let buffer = create_test_buffer::<T>(SIZE);
315+
let mask = generate_mask(SIZE, 0.90, Pattern::Random);
316+
(buffer, mask)
317+
})
318+
.bench_values(|(mut buffer, mask)| {
319+
buffer.in_place_filter(&mask);
320+
divan::black_box(buffer);
321+
});
322+
}
323+
}

vortex-compute/src/filter/buffer.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ const FILTER_SLICES_SELECTIVITY_THRESHOLD: f64 = 0.8;
1111

1212
impl<T: Copy> Filter for Buffer<T> {
1313
fn filter(&self, mask: &Mask) -> Self {
14-
assert_eq!(mask.len(), self.len());
14+
assert_eq!(
15+
mask.len(),
16+
self.len(),
17+
"Selection mask length must equal the buffer length"
18+
);
19+
1520
match mask {
1621
Mask::AllTrue(_) => self.clone(),
1722
Mask::AllFalse(_) => Self::empty(),

0 commit comments

Comments
 (0)