Skip to content

Commit 2f09c15

Browse files
chore[vx]: more decompression throughput benchmarks (#5067)
Signed-off-by: Joe Isaacs <[email protected]>
1 parent 44b1aa5 commit 2f09c15

File tree

3 files changed

+348
-16
lines changed

3 files changed

+348
-16
lines changed

vortex/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,8 @@ unstable_encodings = ["vortex-btrblocks/unstable_encodings"]
8787
name = "single_encoding_throughput"
8888
harness = false
8989
test = false
90+
91+
[[bench]]
92+
name = "common_encoding_tree_throughput"
93+
harness = false
94+
test = false
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
#![allow(clippy::unwrap_used)]
5+
#![allow(unexpected_cfgs)]
6+
7+
use divan::Bencher;
8+
#[cfg(not(codspeed))]
9+
use divan::counter::BytesCount;
10+
use mimalloc::MiMalloc;
11+
use rand::{Rng, SeedableRng};
12+
use vortex::arrays::{PrimitiveArray, TemporalArray, VarBinArray, VarBinViewArray};
13+
use vortex::compute::cast;
14+
use vortex::dtype::datetime::TimeUnit;
15+
use vortex::dtype::{DType, PType};
16+
use vortex::encodings::alp::alp_encode;
17+
use vortex::encodings::datetime_parts::{DateTimePartsArray, split_temporal};
18+
use vortex::encodings::dict::DictArray;
19+
use vortex::encodings::fastlanes::FoRArray;
20+
use vortex::encodings::fsst::{FSSTArray, fsst_compress, fsst_train_compressor};
21+
use vortex::encodings::runend::RunEndArray;
22+
use vortex::vtable::ValidityHelper;
23+
use vortex::{Array, ArrayRef, IntoArray, ToCanonical};
24+
use vortex_fastlanes::BitPackedArray;
25+
26+
#[global_allocator]
27+
static GLOBAL: MiMalloc = MiMalloc;
28+
29+
fn main() {
30+
divan::main();
31+
}
32+
33+
const NUM_VALUES: u64 = 1_000_000;
34+
35+
// Helper macro to conditionally add counter based on codspeed cfg
36+
macro_rules! with_counter {
37+
($bencher:expr, $bytes:expr) => {{
38+
#[cfg(not(codspeed))]
39+
let bencher = $bencher.counter(BytesCount::new($bytes));
40+
#[cfg(codspeed)]
41+
let bencher = {
42+
let _ = $bytes; // Consume the bytes value to avoid unused variable warning
43+
$bencher
44+
};
45+
bencher
46+
}};
47+
}
48+
49+
// Setup functions
50+
fn setup_primitive_arrays() -> (PrimitiveArray, PrimitiveArray, PrimitiveArray) {
51+
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
52+
let uint_array =
53+
PrimitiveArray::from_iter((0..NUM_VALUES).map(|_| rng.random_range(42u32..256)));
54+
let int_array = cast(uint_array.as_ref(), PType::I32.into())
55+
.unwrap()
56+
.to_primitive();
57+
let float_array = cast(uint_array.as_ref(), PType::F64.into())
58+
.unwrap()
59+
.to_primitive();
60+
(uint_array, int_array, float_array)
61+
}
62+
63+
// Encoding tree setup functions
64+
65+
/// Create FoR <- BitPacked encoding tree for u64
66+
fn setup_for_bp_u64() -> ArrayRef {
67+
let (uint_array, ..) = setup_primitive_arrays();
68+
let compressed = FoRArray::encode(uint_array).unwrap();
69+
let inner = compressed.encoded();
70+
let bp = BitPackedArray::encode(inner, 8).unwrap();
71+
FoRArray::try_new(bp.into_array(), compressed.reference_scalar().clone())
72+
.unwrap()
73+
.into_array()
74+
}
75+
76+
/// Create ALP <- FoR <- BitPacked encoding tree for f64
77+
fn setup_alp_for_bp_f64() -> ArrayRef {
78+
let (_, _, float_array) = setup_primitive_arrays();
79+
let alp_compressed = alp_encode(&float_array, None).unwrap();
80+
81+
// Manually construct ALP <- FoR <- BitPacked tree
82+
let for_array = FoRArray::encode(alp_compressed.encoded().to_primitive()).unwrap();
83+
let inner = for_array.encoded();
84+
let bp = BitPackedArray::encode(inner, 8).unwrap();
85+
let for_with_bp =
86+
FoRArray::try_new(bp.into_array(), for_array.reference_scalar().clone()).unwrap();
87+
88+
vortex::encodings::alp::ALPArray::try_new(
89+
for_with_bp.into_array(),
90+
alp_compressed.exponents(),
91+
alp_compressed.patches().cloned(),
92+
)
93+
.unwrap()
94+
.into_array()
95+
}
96+
97+
/// Create Dict <- VarBinView encoding tree for strings with BitPacked codes
98+
#[allow(clippy::cast_possible_truncation)]
99+
fn setup_dict_varbinview_string() -> ArrayRef {
100+
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
101+
102+
// Create unique values (0.005% uniqueness = 50 unique strings)
103+
let num_unique = ((NUM_VALUES as f64) * 0.00005) as usize;
104+
let unique_strings: Vec<String> = (0..num_unique)
105+
.map(|_| {
106+
(0..8)
107+
.map(|_| (rng.random_range(b'a'..=b'z')) as char)
108+
.collect()
109+
})
110+
.collect();
111+
112+
// Create codes array (random indices into unique values)
113+
let codes: Vec<u32> = (0..NUM_VALUES)
114+
.map(|_| rng.random_range(0..num_unique as u32))
115+
.collect();
116+
let codes_prim = PrimitiveArray::from_iter(codes);
117+
118+
// Compress codes with BitPacked (6 bits should be enough for ~50 unique values)
119+
let codes_bp = BitPackedArray::encode(codes_prim.as_ref(), 6)
120+
.unwrap()
121+
.into_array();
122+
123+
// Create values array
124+
let values_array = VarBinViewArray::from_iter_str(unique_strings).into_array();
125+
126+
DictArray::try_new(codes_bp, values_array)
127+
.unwrap()
128+
.into_array()
129+
}
130+
131+
/// Create RunEnd <- FoR <- BitPacked encoding tree for u32
132+
#[allow(clippy::cast_possible_truncation)]
133+
fn setup_runend_for_bp_u32() -> ArrayRef {
134+
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
135+
// Create data with runs of repeated values
136+
let mut values = Vec::with_capacity(NUM_VALUES as usize);
137+
let mut current_value = rng.random_range(0u32..100);
138+
let mut run_length = 0;
139+
140+
for _ in 0..NUM_VALUES {
141+
if run_length == 0 {
142+
current_value = rng.random_range(0u32..100);
143+
run_length = rng.random_range(1..1000);
144+
}
145+
values.push(current_value);
146+
run_length -= 1;
147+
}
148+
149+
let prim_array = PrimitiveArray::from_iter(values);
150+
let runend = RunEndArray::encode(prim_array.into_array()).unwrap();
151+
152+
// Compress the ends with FoR <- BitPacked
153+
let ends_prim = runend.ends().to_primitive();
154+
let ends_for = FoRArray::encode(ends_prim).unwrap();
155+
let ends_inner = ends_for.encoded();
156+
let ends_bp = BitPackedArray::encode(ends_inner, 8).unwrap();
157+
let compressed_ends =
158+
FoRArray::try_new(ends_bp.into_array(), ends_for.reference_scalar().clone())
159+
.unwrap()
160+
.into_array();
161+
162+
// Compress the values with BitPacked
163+
let values_prim = runend.values().to_primitive();
164+
let compressed_values = BitPackedArray::encode(values_prim.as_ref(), 8)
165+
.unwrap()
166+
.into_array();
167+
168+
RunEndArray::try_new(compressed_ends, compressed_values)
169+
.unwrap()
170+
.into_array()
171+
}
172+
173+
/// Create Dict <- FSST <- VarBin encoding tree for strings
174+
#[allow(clippy::cast_possible_truncation)]
175+
fn setup_dict_fsst_varbin_string() -> ArrayRef {
176+
let mut rng = rand::rngs::StdRng::seed_from_u64(43);
177+
178+
// Create unique values (1% uniqueness = 10,000 unique strings)
179+
let num_unique = ((NUM_VALUES as f64) * 0.01) as usize;
180+
let unique_strings: Vec<String> = (0..num_unique)
181+
.map(|_| {
182+
(0..8)
183+
.map(|_| (rng.random_range(b'a'..=b'z')) as char)
184+
.collect()
185+
})
186+
.collect();
187+
188+
// Train and compress unique values with FSST
189+
let unique_varbinview = VarBinViewArray::from_iter_str(unique_strings).into_array();
190+
let fsst_compressor = fsst_train_compressor(&unique_varbinview).unwrap();
191+
let fsst_values = fsst_compress(&unique_varbinview, &fsst_compressor).unwrap();
192+
193+
// Create codes array (random indices into unique values)
194+
let codes: Vec<u32> = (0..NUM_VALUES)
195+
.map(|_| rng.random_range(0..num_unique as u32))
196+
.collect();
197+
let codes_array = PrimitiveArray::from_iter(codes).into_array();
198+
199+
DictArray::try_new(codes_array, fsst_values.into_array())
200+
.unwrap()
201+
.into_array()
202+
}
203+
204+
/// Create Dict <- FSST <- VarBin <- BitPacked encoding tree for strings
205+
/// Compress the VarBin offsets inside FSST with BitPacked
206+
#[allow(clippy::cast_possible_truncation)]
207+
fn setup_dict_fsst_varbin_bp_string() -> ArrayRef {
208+
let mut rng = rand::rngs::StdRng::seed_from_u64(45);
209+
210+
// Create unique values (1% uniqueness = 10,000 unique strings)
211+
let num_unique = ((NUM_VALUES as f64) * 0.01) as usize;
212+
let unique_strings: Vec<String> = (0..num_unique)
213+
.map(|_| {
214+
(0..8)
215+
.map(|_| (rng.random_range(b'a'..=b'z')) as char)
216+
.collect()
217+
})
218+
.collect();
219+
220+
// Train and compress unique values with FSST
221+
let unique_varbinview = VarBinViewArray::from_iter_str(unique_strings).into_array();
222+
let fsst_compressor = fsst_train_compressor(&unique_varbinview).unwrap();
223+
let fsst = fsst_compress(&unique_varbinview, &fsst_compressor).unwrap();
224+
225+
// Compress the VarBin offsets with BitPacked
226+
let codes = fsst.codes();
227+
let offsets_prim = codes.offsets().to_primitive();
228+
let offsets_bp = BitPackedArray::encode(offsets_prim.as_ref(), 20).unwrap();
229+
230+
// Rebuild VarBin with compressed offsets
231+
let compressed_codes = VarBinArray::try_new(
232+
offsets_bp.into_array(),
233+
codes.bytes().clone(),
234+
codes.dtype().clone(),
235+
codes.validity().clone(),
236+
)
237+
.unwrap();
238+
239+
// Rebuild FSST with compressed codes
240+
let compressed_fsst = FSSTArray::try_new(
241+
fsst.dtype().clone(),
242+
fsst.symbols().clone(),
243+
fsst.symbol_lengths().clone(),
244+
compressed_codes,
245+
fsst.uncompressed_lengths().clone(),
246+
)
247+
.unwrap();
248+
249+
// Create codes array (random indices into unique values)
250+
let dict_codes: Vec<u32> = (0..NUM_VALUES)
251+
.map(|_| rng.random_range(0..num_unique as u32))
252+
.collect();
253+
let codes_array = PrimitiveArray::from_iter(dict_codes).into_array();
254+
255+
DictArray::try_new(codes_array, compressed_fsst.into_array())
256+
.unwrap()
257+
.into_array()
258+
}
259+
260+
/// Create DateTimeParts <- FoR <- BitPacked encoding tree
261+
fn setup_datetime_for_bp() -> ArrayRef {
262+
// Create timestamp data (microseconds since epoch)
263+
let mut rng = rand::rngs::StdRng::seed_from_u64(123);
264+
let base_timestamp = 1_600_000_000_000_000i64; // Sept 2020 in microseconds
265+
let timestamps: Vec<i64> = (0..NUM_VALUES)
266+
.map(|_| base_timestamp + rng.random_range(0..86_400_000_000)) // Random times within a day
267+
.collect();
268+
269+
let ts_array = PrimitiveArray::from_iter(timestamps).into_array();
270+
271+
// Create TemporalArray with microsecond timestamps
272+
let temporal_array = TemporalArray::new_timestamp(ts_array, TimeUnit::Microseconds, None);
273+
274+
// Split into days, seconds, subseconds
275+
let parts = split_temporal(temporal_array.clone()).unwrap();
276+
277+
// Compress days with FoR <- BitPacked
278+
let days_prim = parts.days.to_primitive();
279+
let days_for = FoRArray::encode(days_prim).unwrap();
280+
let days_inner = days_for.encoded();
281+
let days_bp = BitPackedArray::encode(days_inner, 16).unwrap();
282+
let compressed_days =
283+
FoRArray::try_new(days_bp.into_array(), days_for.reference_scalar().clone())
284+
.unwrap()
285+
.into_array();
286+
287+
// Compress seconds with FoR <- BitPacked
288+
let seconds_prim = parts.seconds.to_primitive();
289+
let seconds_for = FoRArray::encode(seconds_prim).unwrap();
290+
let seconds_inner = seconds_for.encoded();
291+
let seconds_bp = BitPackedArray::encode(seconds_inner, 17).unwrap();
292+
let compressed_seconds = FoRArray::try_new(
293+
seconds_bp.into_array(),
294+
seconds_for.reference_scalar().clone(),
295+
)
296+
.unwrap()
297+
.into_array();
298+
299+
// Compress subseconds with FoR <- BitPacked
300+
let subseconds_prim = parts.subseconds.to_primitive();
301+
let subseconds_for = FoRArray::encode(subseconds_prim).unwrap();
302+
let subseconds_inner = subseconds_for.encoded();
303+
let subseconds_bp = BitPackedArray::encode(subseconds_inner, 20).unwrap();
304+
let compressed_subseconds = FoRArray::try_new(
305+
subseconds_bp.into_array(),
306+
subseconds_for.reference_scalar().clone(),
307+
)
308+
.unwrap()
309+
.into_array();
310+
311+
DateTimePartsArray::try_new(
312+
DType::Extension(temporal_array.ext_dtype()),
313+
compressed_days,
314+
compressed_seconds,
315+
compressed_subseconds,
316+
)
317+
.unwrap()
318+
.into_array()
319+
}
320+
321+
// Complex encoding tree benchmarks
322+
323+
/// Benchmark decompression of various encoding trees
324+
#[divan::bench(
325+
args = [
326+
("for_bp_u64", setup_for_bp_u64 as fn() -> ArrayRef),
327+
("alp_for_bp_f64", setup_alp_for_bp_f64 as fn() -> ArrayRef),
328+
("dict_varbinview_string", setup_dict_varbinview_string as fn() -> ArrayRef),
329+
("runend_for_bp_u32", setup_runend_for_bp_u32 as fn() -> ArrayRef),
330+
("dict_fsst_varbin_string", setup_dict_fsst_varbin_string as fn() -> ArrayRef),
331+
("dict_fsst_varbin_bp_string", setup_dict_fsst_varbin_bp_string as fn() -> ArrayRef),
332+
("datetime_for_bp", setup_datetime_for_bp as fn() -> ArrayRef),
333+
]
334+
)]
335+
fn decompress(bencher: Bencher, (name, setup_fn): (&str, fn() -> ArrayRef)) {
336+
let _ = name; // Used by divan for display
337+
let compressed = setup_fn();
338+
let nbytes = compressed.nbytes();
339+
340+
with_counter!(bencher, nbytes)
341+
.with_inputs(|| compressed.clone())
342+
.bench_values(|a| a.to_canonical());
343+
}

vortex/benches/single_encoding_throughput.rs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ use vortex::encodings::runend::RunEndArray;
2222
use vortex::encodings::zigzag::zigzag_encode;
2323
use vortex::encodings::zstd::ZstdArray;
2424
use vortex::{IntoArray, ToCanonical};
25-
use vortex_fastlanes::BitPackedArray;
2625

2726
#[global_allocator]
2827
static GLOBAL: MiMalloc = MiMalloc;
@@ -156,21 +155,6 @@ fn bench_for_compress_i32(bencher: Bencher) {
156155
.bench_values(|a| FoRArray::encode(a).unwrap());
157156
}
158157

159-
#[divan::bench(name = "for_decompress_u64")]
160-
fn bench_for_decompress_u64(bencher: Bencher) {
161-
let (uint_array, ..) = setup_primitive_arrays();
162-
163-
let compressed = FoRArray::encode(uint_array).unwrap();
164-
let inner = compressed.encoded();
165-
let bp = BitPackedArray::encode(inner, 8).unwrap();
166-
let compressed =
167-
FoRArray::try_new(bp.into_array(), compressed.reference_scalar().clone()).unwrap();
168-
169-
with_counter!(bencher, NUM_VALUES * 4)
170-
.with_inputs(|| compressed.clone())
171-
.bench_values(|a| a.to_canonical());
172-
}
173-
174158
#[divan::bench(name = "for_decompress_i32")]
175159
fn bench_for_decompress_i32(bencher: Bencher) {
176160
let (_, int_array, _) = setup_primitive_arrays();

0 commit comments

Comments
 (0)