Skip to content

Commit c292ddb

Browse files
perf: add benchmarks on uniform, sorted, clustered, sparse, constant, geometric data for compression/decompression/round-trip/rate (fast-pack#51)
I asked my LLM (claude) of choice to write a benchmark for this since we don't currently have one and since this is fairly manual work. This is mostly done to check a few assumptions around fast-pack#49 I reviewed every line and fixed the issues that were caused. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 87f250e commit c292ddb

2 files changed

Lines changed: 349 additions & 1 deletion

File tree

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ edition = "2021"
1111
license = "MIT OR Apache-2.0"
1212
keywords = ["fastpfor", "compression"]
1313
categories = ["compression"]
14-
rust-version = "1.85.0"
14+
rust-version = "1.86.0"
15+
16+
[[bench]]
17+
name = "fastpfor_benchmark"
18+
required-features = ["rust"]
19+
harness = false
1520

1621
[features]
1722
# Eventually we may want to build without the C++ bindings by default.
@@ -29,6 +34,7 @@ cmake = { version = "0.1.57", optional = true }
2934
cxx-build = { version = "1.0.194", optional = true }
3035

3136
[dev-dependencies]
37+
criterion = "0.8"
3238
rand = "0.10.0"
3339

3440
[lints.rust]

benches/fastpfor_benchmark.rs

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
use core::ops::Range;
2+
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
3+
use fastpfor::rust::{FastPFOR, Integer, BLOCK_SIZE_128, BLOCK_SIZE_256, DEFAULT_PAGE_SIZE};
4+
use rand::rngs::StdRng;
5+
use rand::{RngExt as _, SeedableRng};
6+
use std::hint::black_box;
7+
use std::io::Cursor;
8+
9+
const SIZES: &[usize; 2] = &[1024, 4096];
10+
const SEED: u64 = 456;
11+
12+
type DataGeneratorFn = fn(usize) -> Vec<u32>;
13+
14+
/// Generate uniformly distributed random data
15+
fn generate_uniform_data_from_range(size: usize, value_range: Range<u32>) -> Vec<u32> {
16+
let mut rng = StdRng::seed_from_u64(SEED);
17+
(0..size)
18+
.map(|_| rng.random_range(value_range.clone()))
19+
.collect()
20+
}
21+
22+
fn generate_uniform_data_small_value_distribution(size: usize) -> Vec<u32> {
23+
generate_uniform_data_from_range(size, 0..1000)
24+
}
25+
26+
fn generate_uniform_data_large_value_distribution(size: usize) -> Vec<u32> {
27+
generate_uniform_data_from_range(size, 0..u32::MAX)
28+
}
29+
30+
/// Generate clustered data - values tend to cluster around changing base values
31+
fn generate_clustered_data(size: usize) -> Vec<u32> {
32+
let mut rng = StdRng::seed_from_u64(SEED);
33+
let mut data = Vec::with_capacity(size);
34+
let mut base = 0u32;
35+
36+
for _ in 0..size {
37+
// 10% chance to jump to a new cluster
38+
if rng.random_bool(0.1) {
39+
base = rng.random_range(0..1000);
40+
}
41+
data.push(base + rng.random_range(0..10));
42+
}
43+
data
44+
}
45+
46+
/// Generate sequential/sorted data - ideal for compression
47+
fn generate_sequential_data(size: usize) -> Vec<u32> {
48+
(0..size as u32).collect()
49+
}
50+
51+
/// Generate sparse data - mostly zeros with occasional random values
52+
fn generate_sparse_data(size: usize) -> Vec<u32> {
53+
let mut rng = StdRng::seed_from_u64(SEED);
54+
(0..size)
55+
.map(|_| {
56+
if rng.random_bool(0.9) {
57+
0
58+
} else {
59+
rng.random()
60+
}
61+
})
62+
.collect()
63+
}
64+
65+
/// Generate constant data - best case for compression
66+
fn generate_constant_data(size: usize) -> Vec<u32> {
67+
vec![SEED as u32; size]
68+
}
69+
70+
/// Generate data with powers of two
71+
fn generate_geometric_data(size: usize) -> Vec<u32> {
72+
(0..size).map(|i| 1u32 << (i % 30)).collect()
73+
}
74+
75+
/// Helper function to compress data and return the compressed size
76+
fn compress_data(codec: &mut FastPFOR, data: &[u32]) -> usize {
77+
let mut compressed = vec![0u32; data.len() * 2];
78+
let mut input_offset = Cursor::new(0);
79+
let mut output_offset = Cursor::new(0);
80+
81+
codec
82+
.compress(
83+
data,
84+
data.len() as u32,
85+
&mut input_offset,
86+
&mut compressed,
87+
&mut output_offset,
88+
)
89+
.unwrap();
90+
91+
output_offset.position() as usize
92+
}
93+
94+
/// Helper function to compress data and return compressed buffer
95+
fn prepare_compressed_data(data: &[u32], block_size: u32) -> Vec<u32> {
96+
let mut codec = FastPFOR::new(DEFAULT_PAGE_SIZE, block_size);
97+
let mut compressed = vec![0u32; data.len() * 2];
98+
let mut input_offset = Cursor::new(0);
99+
let mut output_offset = Cursor::new(0);
100+
101+
codec
102+
.compress(
103+
data,
104+
data.len() as u32,
105+
&mut input_offset,
106+
&mut compressed,
107+
&mut output_offset,
108+
)
109+
.unwrap();
110+
111+
let compressed_size = output_offset.position() as usize;
112+
compressed.truncate(compressed_size);
113+
compressed
114+
}
115+
116+
/// Helper function to decompress data
117+
fn decompress_data(codec: &mut FastPFOR, compressed: &[u32], original_size: usize) -> usize {
118+
let mut decompressed = vec![0u32; original_size];
119+
let mut input_offset = Cursor::new(0);
120+
let mut output_offset = Cursor::new(0);
121+
122+
codec
123+
.uncompress(
124+
compressed,
125+
compressed.len() as u32,
126+
&mut input_offset,
127+
&mut decompressed,
128+
&mut output_offset,
129+
)
130+
.unwrap();
131+
132+
output_offset.position() as usize
133+
}
134+
135+
fn benchmark_compression(c: &mut Criterion) {
136+
let mut group = c.benchmark_group("compression");
137+
138+
let patterns: &[(&str, DataGeneratorFn)] = &[
139+
(
140+
"uniform_small_value_distribution",
141+
generate_uniform_data_small_value_distribution,
142+
),
143+
(
144+
"uniform_large_value_distribution",
145+
generate_uniform_data_large_value_distribution,
146+
),
147+
("clustered", generate_clustered_data),
148+
("sequential", generate_sequential_data),
149+
("sparse", generate_sparse_data),
150+
];
151+
152+
for &size in SIZES {
153+
for (name, generator) in patterns {
154+
let data = generator(size);
155+
group.throughput(Throughput::Elements(size as u64));
156+
group.bench_with_input(BenchmarkId::new(*name, size), &data, |b, data| {
157+
b.iter(|| {
158+
let mut codec = FastPFOR::default();
159+
black_box(compress_data(&mut codec, black_box(data)))
160+
});
161+
});
162+
}
163+
}
164+
165+
group.finish();
166+
}
167+
168+
fn benchmark_decompression(c: &mut Criterion) {
169+
let mut group = c.benchmark_group("decompression");
170+
171+
let patterns: &[(&str, DataGeneratorFn)] = &[
172+
(
173+
"uniform_small_value_distribution",
174+
generate_uniform_data_small_value_distribution,
175+
),
176+
(
177+
"uniform_large_value_distribution",
178+
generate_uniform_data_large_value_distribution,
179+
),
180+
("clustered", generate_clustered_data),
181+
("sequential", generate_sequential_data),
182+
("sparse", generate_sparse_data),
183+
];
184+
185+
for &size in SIZES {
186+
for (name, generator) in patterns {
187+
let data = generator(size);
188+
let compressed = prepare_compressed_data(&data, BLOCK_SIZE_128);
189+
190+
group.throughput(Throughput::Elements(size as u64));
191+
group.bench_with_input(
192+
BenchmarkId::new(*name, size),
193+
&(compressed, size),
194+
|b, (compressed, size)| {
195+
b.iter(|| {
196+
let mut codec = FastPFOR::default();
197+
black_box(decompress_data(&mut codec, black_box(compressed), *size))
198+
});
199+
},
200+
);
201+
}
202+
}
203+
204+
group.finish();
205+
}
206+
207+
fn benchmark_roundtrip(c: &mut Criterion) {
208+
let mut group = c.benchmark_group("roundtrip");
209+
210+
for &size in SIZES {
211+
let data = generate_uniform_data_small_value_distribution(size);
212+
213+
group.throughput(Throughput::Elements(size as u64));
214+
group.bench_with_input(
215+
BenchmarkId::new("compress_decompress", size),
216+
&data,
217+
|b, data| {
218+
b.iter(|| {
219+
let mut codec1 = FastPFOR::default();
220+
let mut codec2 = FastPFOR::default();
221+
let mut compressed = vec![0u32; data.len() * 2];
222+
let mut decompressed = vec![0u32; data.len()];
223+
let mut input_offset = Cursor::new(0);
224+
let mut output_offset = Cursor::new(0);
225+
226+
codec1
227+
.compress(
228+
black_box(data),
229+
data.len() as u32,
230+
&mut input_offset,
231+
&mut compressed,
232+
&mut output_offset,
233+
)
234+
.unwrap();
235+
236+
input_offset.set_position(0);
237+
let compressed_len = output_offset.position();
238+
output_offset.set_position(0);
239+
240+
codec2
241+
.uncompress(
242+
&compressed,
243+
data.len() as u32,
244+
&mut input_offset,
245+
&mut decompressed,
246+
&mut output_offset,
247+
)
248+
.unwrap();
249+
250+
black_box((compressed_len, output_offset.position()))
251+
});
252+
},
253+
);
254+
}
255+
256+
group.finish();
257+
}
258+
259+
fn benchmark_block_sizes(c: &mut Criterion) {
260+
let mut group = c.benchmark_group("block_sizes");
261+
262+
let size = *SIZES.last().unwrap();
263+
let data = generate_uniform_data_small_value_distribution(size);
264+
265+
let block_sizes = [BLOCK_SIZE_128, BLOCK_SIZE_256];
266+
267+
for block_size in block_sizes {
268+
group.throughput(Throughput::Elements(size as u64));
269+
group.bench_function(format!("compress_{block_size}"), |b| {
270+
b.iter(|| {
271+
let mut codec = FastPFOR::new(DEFAULT_PAGE_SIZE, block_size);
272+
black_box(compress_data(&mut codec, black_box(&data)))
273+
});
274+
});
275+
}
276+
277+
// Also benchmark decompression performance for each block size
278+
for block_size in block_sizes {
279+
// Pre-compress the data once for this block size so the decompression
280+
// benchmark measures only decompression work inside `b.iter`.
281+
let compressed = prepare_compressed_data(&data, block_size);
282+
283+
group.throughput(Throughput::Elements(size as u64));
284+
group.bench_function(format!("decompress_{block_size}"), |b| {
285+
b.iter(|| {
286+
let mut codec = FastPFOR::new(DEFAULT_PAGE_SIZE, block_size);
287+
black_box(decompress_data(&mut codec, black_box(&compressed), size));
288+
});
289+
});
290+
}
291+
group.finish();
292+
}
293+
294+
fn benchmark_compression_ratio(c: &mut Criterion) {
295+
let mut group = c.benchmark_group("compression_ratio");
296+
group.sample_size(20); // Fewer samples since we're measuring ratio, not just speed
297+
298+
let size = *SIZES.last().unwrap();
299+
let patterns: &[(&str, DataGeneratorFn)] = &[
300+
(
301+
"uniform_small_distribution",
302+
generate_uniform_data_small_value_distribution,
303+
),
304+
(
305+
"uniform_large_distribution",
306+
generate_uniform_data_large_value_distribution,
307+
),
308+
("clustered", generate_clustered_data),
309+
("sequential", generate_sequential_data),
310+
("sparse", generate_sparse_data),
311+
("constant", generate_constant_data),
312+
("geometric", generate_geometric_data),
313+
];
314+
315+
for (name, data_fn) in patterns {
316+
let data = data_fn(size);
317+
group.bench_function(*name, |b| {
318+
b.iter(|| {
319+
let mut codec = FastPFOR::default();
320+
let compressed_size = compress_data(&mut codec, black_box(&data));
321+
#[expect(
322+
clippy::cast_precision_loss,
323+
reason = "Loss of precision is acceptable for compression ratio calculation"
324+
)]
325+
let ratio = data.len() as f64 / compressed_size as f64;
326+
black_box(ratio)
327+
});
328+
});
329+
}
330+
331+
group.finish();
332+
}
333+
334+
criterion_group!(
335+
benches,
336+
benchmark_compression,
337+
benchmark_decompression,
338+
benchmark_roundtrip,
339+
benchmark_block_sizes,
340+
benchmark_compression_ratio
341+
);
342+
criterion_main!(benches);

0 commit comments

Comments
 (0)