Skip to content

Commit 3e086fa

Browse files
committed
add in-place filter for BufferMut + benchmarks
Signed-off-by: Connor Tsui <[email protected]>
1 parent 86f3bf9 commit 3e086fa

File tree

5 files changed

+572
-0
lines changed

5 files changed

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

0 commit comments

Comments
 (0)