Skip to content

Commit ba77972

Browse files
perf[runend]: run_end_bool decompress (#6229)
speed up runend bool decompress --------- Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 27f8acf commit ba77972

11 files changed

Lines changed: 871 additions & 120 deletions

File tree

encodings/runend/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ harness = false
4848
[[bench]]
4949
name = "run_end_compress"
5050
harness = false
51+
52+
[[bench]]
53+
name = "run_end_decode"
54+
harness = false
Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
#![allow(clippy::unwrap_used, clippy::cast_possible_truncation)]
5+
6+
use std::fmt;
7+
8+
use divan::Bencher;
9+
use vortex_array::arrays::BoolArray;
10+
use vortex_array::arrays::PrimitiveArray;
11+
use vortex_array::compute::warm_up_vtables;
12+
use vortex_array::validity::Validity;
13+
use vortex_buffer::BitBuffer;
14+
use vortex_buffer::BufferMut;
15+
use vortex_runend::decompress_bool::runend_decode_bools;
16+
17+
fn main() {
18+
warm_up_vtables();
19+
divan::main();
20+
}
21+
22+
/// Distribution types for bool benchmarks
23+
#[derive(Clone, Copy)]
24+
enum BoolDistribution {
25+
/// Alternating true/false (50/50)
26+
Alternating,
27+
/// Mostly true (90% true runs)
28+
MostlyTrue,
29+
/// Mostly false (90% false runs)
30+
MostlyFalse,
31+
/// All true
32+
AllTrue,
33+
/// All false
34+
AllFalse,
35+
}
36+
37+
impl fmt::Display for BoolDistribution {
38+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39+
match self {
40+
BoolDistribution::Alternating => write!(f, "alternating"),
41+
BoolDistribution::MostlyTrue => write!(f, "mostly_true"),
42+
BoolDistribution::MostlyFalse => write!(f, "mostly_false"),
43+
BoolDistribution::AllTrue => write!(f, "all_true"),
44+
BoolDistribution::AllFalse => write!(f, "all_false"),
45+
}
46+
}
47+
}
48+
49+
#[derive(Clone, Copy)]
50+
struct BoolBenchArgs {
51+
total_length: usize,
52+
avg_run_length: usize,
53+
distribution: BoolDistribution,
54+
}
55+
56+
impl fmt::Display for BoolBenchArgs {
57+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58+
write!(
59+
f,
60+
"{}_{}_{}",
61+
self.total_length, self.avg_run_length, self.distribution
62+
)
63+
}
64+
}
65+
66+
/// Creates bool test data with configurable distribution
67+
fn create_bool_test_data(
68+
total_length: usize,
69+
avg_run_length: usize,
70+
distribution: BoolDistribution,
71+
) -> (PrimitiveArray, BoolArray) {
72+
let mut ends = BufferMut::<u32>::with_capacity(total_length / avg_run_length + 1);
73+
let mut values = Vec::with_capacity(total_length / avg_run_length + 1);
74+
75+
let mut pos = 0usize;
76+
let mut run_index = 0usize;
77+
78+
while pos < total_length {
79+
let run_len = avg_run_length.min(total_length - pos);
80+
pos += run_len;
81+
ends.push(pos as u32);
82+
83+
let val = match distribution {
84+
BoolDistribution::Alternating => run_index.is_multiple_of(2),
85+
BoolDistribution::MostlyTrue => !run_index.is_multiple_of(10), // 90% true
86+
BoolDistribution::MostlyFalse => run_index.is_multiple_of(10), // 10% true (90% false)
87+
BoolDistribution::AllTrue => true,
88+
BoolDistribution::AllFalse => false,
89+
};
90+
values.push(val);
91+
run_index += 1;
92+
}
93+
94+
(
95+
PrimitiveArray::new(ends.freeze(), Validity::NonNullable),
96+
BoolArray::from(BitBuffer::from(values)),
97+
)
98+
}
99+
100+
// Medium size: 10k elements with various run lengths and distributions
101+
const BOOL_ARGS: &[BoolBenchArgs] = &[
102+
BoolBenchArgs {
103+
total_length: 10_000,
104+
avg_run_length: 2,
105+
distribution: BoolDistribution::Alternating,
106+
},
107+
BoolBenchArgs {
108+
total_length: 10_000,
109+
avg_run_length: 10,
110+
distribution: BoolDistribution::Alternating,
111+
},
112+
BoolBenchArgs {
113+
total_length: 10_000,
114+
avg_run_length: 100,
115+
distribution: BoolDistribution::Alternating,
116+
},
117+
BoolBenchArgs {
118+
total_length: 10_000,
119+
avg_run_length: 1000,
120+
distribution: BoolDistribution::Alternating,
121+
},
122+
BoolBenchArgs {
123+
total_length: 10_000,
124+
avg_run_length: 2,
125+
distribution: BoolDistribution::MostlyTrue,
126+
},
127+
BoolBenchArgs {
128+
total_length: 10_000,
129+
avg_run_length: 10,
130+
distribution: BoolDistribution::MostlyTrue,
131+
},
132+
BoolBenchArgs {
133+
total_length: 10_000,
134+
avg_run_length: 100,
135+
distribution: BoolDistribution::MostlyTrue,
136+
},
137+
BoolBenchArgs {
138+
total_length: 10_000,
139+
avg_run_length: 1000,
140+
distribution: BoolDistribution::MostlyTrue,
141+
},
142+
BoolBenchArgs {
143+
total_length: 10_000,
144+
avg_run_length: 2,
145+
distribution: BoolDistribution::MostlyFalse,
146+
},
147+
BoolBenchArgs {
148+
total_length: 10_000,
149+
avg_run_length: 10,
150+
distribution: BoolDistribution::MostlyFalse,
151+
},
152+
BoolBenchArgs {
153+
total_length: 10_000,
154+
avg_run_length: 100,
155+
distribution: BoolDistribution::MostlyFalse,
156+
},
157+
BoolBenchArgs {
158+
total_length: 10_000,
159+
avg_run_length: 1000,
160+
distribution: BoolDistribution::MostlyFalse,
161+
},
162+
BoolBenchArgs {
163+
total_length: 10_000,
164+
avg_run_length: 2,
165+
distribution: BoolDistribution::AllTrue,
166+
},
167+
BoolBenchArgs {
168+
total_length: 10_000,
169+
avg_run_length: 10,
170+
distribution: BoolDistribution::AllTrue,
171+
},
172+
BoolBenchArgs {
173+
total_length: 10_000,
174+
avg_run_length: 100,
175+
distribution: BoolDistribution::AllTrue,
176+
},
177+
BoolBenchArgs {
178+
total_length: 10_000,
179+
avg_run_length: 1000,
180+
distribution: BoolDistribution::AllTrue,
181+
},
182+
BoolBenchArgs {
183+
total_length: 10_000,
184+
avg_run_length: 2,
185+
distribution: BoolDistribution::AllFalse,
186+
},
187+
BoolBenchArgs {
188+
total_length: 10_000,
189+
avg_run_length: 10,
190+
distribution: BoolDistribution::AllFalse,
191+
},
192+
BoolBenchArgs {
193+
total_length: 10_000,
194+
avg_run_length: 100,
195+
distribution: BoolDistribution::AllFalse,
196+
},
197+
BoolBenchArgs {
198+
total_length: 10_000,
199+
avg_run_length: 1000,
200+
distribution: BoolDistribution::AllFalse,
201+
},
202+
];
203+
204+
#[divan::bench(args = BOOL_ARGS)]
205+
fn decode_bool(bencher: Bencher, args: BoolBenchArgs) {
206+
let BoolBenchArgs {
207+
total_length,
208+
avg_run_length,
209+
distribution,
210+
} = args;
211+
let (ends, values) = create_bool_test_data(total_length, avg_run_length, distribution);
212+
bencher
213+
.with_inputs(|| (ends.clone(), values.clone()))
214+
.bench_refs(|(ends, values)| {
215+
runend_decode_bools(ends.clone(), values.clone(), 0, total_length)
216+
});
217+
}
218+
219+
/// Validity distribution for nullable benchmarks
220+
#[derive(Clone, Copy)]
221+
enum ValidityDistribution {
222+
/// 90% valid
223+
MostlyValid,
224+
/// 50% valid
225+
HalfValid,
226+
/// 10% valid
227+
MostlyNull,
228+
}
229+
230+
impl fmt::Display for ValidityDistribution {
231+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232+
match self {
233+
ValidityDistribution::MostlyValid => write!(f, "mostly_valid"),
234+
ValidityDistribution::HalfValid => write!(f, "half_valid"),
235+
ValidityDistribution::MostlyNull => write!(f, "mostly_null"),
236+
}
237+
}
238+
}
239+
240+
#[derive(Clone, Copy)]
241+
struct NullableBoolBenchArgs {
242+
total_length: usize,
243+
avg_run_length: usize,
244+
distribution: BoolDistribution,
245+
validity: ValidityDistribution,
246+
}
247+
248+
impl fmt::Display for NullableBoolBenchArgs {
249+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250+
write!(
251+
f,
252+
"{}_{}_{}_{}",
253+
self.total_length, self.avg_run_length, self.distribution, self.validity
254+
)
255+
}
256+
}
257+
258+
/// Creates nullable bool test data with configurable distribution and validity
259+
fn create_nullable_bool_test_data(
260+
total_length: usize,
261+
avg_run_length: usize,
262+
distribution: BoolDistribution,
263+
validity: ValidityDistribution,
264+
) -> (PrimitiveArray, BoolArray) {
265+
let mut ends = BufferMut::<u32>::with_capacity(total_length / avg_run_length + 1);
266+
let mut values = Vec::with_capacity(total_length / avg_run_length + 1);
267+
let mut validity_bits = Vec::with_capacity(total_length / avg_run_length + 1);
268+
269+
let mut pos = 0usize;
270+
let mut run_index = 0usize;
271+
272+
while pos < total_length {
273+
let run_len = avg_run_length.min(total_length - pos);
274+
pos += run_len;
275+
ends.push(pos as u32);
276+
277+
let val = match distribution {
278+
BoolDistribution::Alternating => run_index.is_multiple_of(2),
279+
BoolDistribution::MostlyTrue => !run_index.is_multiple_of(10),
280+
BoolDistribution::MostlyFalse => run_index.is_multiple_of(10),
281+
BoolDistribution::AllTrue => true,
282+
BoolDistribution::AllFalse => false,
283+
};
284+
values.push(val);
285+
286+
let is_valid = match validity {
287+
ValidityDistribution::MostlyValid => !run_index.is_multiple_of(10),
288+
ValidityDistribution::HalfValid => run_index.is_multiple_of(2),
289+
ValidityDistribution::MostlyNull => run_index.is_multiple_of(10),
290+
};
291+
validity_bits.push(is_valid);
292+
293+
run_index += 1;
294+
}
295+
296+
(
297+
PrimitiveArray::new(ends.freeze(), Validity::NonNullable),
298+
BoolArray::new(
299+
BitBuffer::from(values),
300+
Validity::from(BitBuffer::from(validity_bits)),
301+
),
302+
)
303+
}
304+
305+
const NULLABLE_BOOL_ARGS: &[NullableBoolBenchArgs] = &[
306+
// Alternating with different validity
307+
NullableBoolBenchArgs {
308+
total_length: 10_000,
309+
avg_run_length: 10,
310+
distribution: BoolDistribution::Alternating,
311+
validity: ValidityDistribution::MostlyValid,
312+
},
313+
NullableBoolBenchArgs {
314+
total_length: 10_000,
315+
avg_run_length: 10,
316+
distribution: BoolDistribution::Alternating,
317+
validity: ValidityDistribution::HalfValid,
318+
},
319+
NullableBoolBenchArgs {
320+
total_length: 10_000,
321+
avg_run_length: 10,
322+
distribution: BoolDistribution::Alternating,
323+
validity: ValidityDistribution::MostlyNull,
324+
},
325+
// MostlyTrue with different validity
326+
NullableBoolBenchArgs {
327+
total_length: 10_000,
328+
avg_run_length: 10,
329+
distribution: BoolDistribution::MostlyTrue,
330+
validity: ValidityDistribution::MostlyValid,
331+
},
332+
NullableBoolBenchArgs {
333+
total_length: 10_000,
334+
avg_run_length: 10,
335+
distribution: BoolDistribution::MostlyTrue,
336+
validity: ValidityDistribution::HalfValid,
337+
},
338+
NullableBoolBenchArgs {
339+
total_length: 10_000,
340+
avg_run_length: 10,
341+
distribution: BoolDistribution::MostlyTrue,
342+
validity: ValidityDistribution::MostlyNull,
343+
},
344+
// Different run lengths with MostlyValid
345+
NullableBoolBenchArgs {
346+
total_length: 10_000,
347+
avg_run_length: 2,
348+
distribution: BoolDistribution::Alternating,
349+
validity: ValidityDistribution::MostlyValid,
350+
},
351+
NullableBoolBenchArgs {
352+
total_length: 10_000,
353+
avg_run_length: 100,
354+
distribution: BoolDistribution::Alternating,
355+
validity: ValidityDistribution::MostlyValid,
356+
},
357+
NullableBoolBenchArgs {
358+
total_length: 10_000,
359+
avg_run_length: 1000,
360+
distribution: BoolDistribution::Alternating,
361+
validity: ValidityDistribution::MostlyValid,
362+
},
363+
];
364+
365+
#[divan::bench(args = NULLABLE_BOOL_ARGS)]
366+
fn decode_bool_nullable(bencher: Bencher, args: NullableBoolBenchArgs) {
367+
let NullableBoolBenchArgs {
368+
total_length,
369+
avg_run_length,
370+
distribution,
371+
validity,
372+
} = args;
373+
let (ends, values) =
374+
create_nullable_bool_test_data(total_length, avg_run_length, distribution, validity);
375+
bencher
376+
.with_inputs(|| (ends.clone(), values.clone()))
377+
.bench_refs(|(ends, values)| {
378+
runend_decode_bools(ends.clone(), values.clone(), 0, total_length)
379+
});
380+
}

0 commit comments

Comments
 (0)