Skip to content

Commit 0a128b6

Browse files
authored
feat(bench): add Arrow IPC benchmark baselines (#9624)
Add `arrow-ipc` as an on-disk format in the compression-size, compression-throughput, and random-access benchmark suites. Compression benchmark runs emit Arrow IPC file sizes, serialization and deserialization timings, and decoded in-memory Arrow bytes as `uncompressed_bytes`. Random-access runs read row-addressable Arrow IPC files. The decoded Arrow byte count supplies logical throughput and compression-ratio estimates. The Arrow IPC rows remain file-format results. The develop workflow emits these records across the existing dataset matrix. --------- Signed-off-by: Will Manning <will@willmanning.io>
1 parent 8610ba8 commit 0a128b6

26 files changed

Lines changed: 403 additions & 31 deletions

File tree

.github/workflows/develop-bench.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ jobs:
156156
VORTEX_EXPERIMENTAL_PATCHED_ARRAY: "1"
157157
FLAT_LAYOUT_INLINE_ARRAY_NODE: "1"
158158
run: |
159-
python3 scripts/compress-split.py --formats parquet,lance,vortex --emit-ingest-records
159+
python3 scripts/compress-split.py --formats arrow-ipc,parquet,lance,vortex --emit-ingest-records
160160
161161
- name: Run ${{ matrix.benchmark.name }} benchmark (per-column-encoder)
162162
if: matrix.benchmark.id == 'string-bench'

Cargo.lock

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

benchmarks/compress-bench/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ publish = false
1717
[dependencies]
1818
anyhow = { workspace = true }
1919
arrow-array = { workspace = true }
20+
arrow-ipc = { workspace = true }
2021
arrow-schema = { workspace = true }
2122
async-trait = { workspace = true }
2223
bytes = { workspace = true }

benchmarks/compress-bench/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
# Compression benchmark
22

3-
Measures compression and decompression throughput, plus resulting file sizes, for Vortex
4-
versus Parquet (and optionally Lance) across a range of datasets: NYC taxi data, several
3+
Measures compression and decompression throughput, plus resulting file sizes, for Vortex,
4+
Parquet, uncompressed Arrow IPC, and optionally Lance.
5+
6+
[Arrow IPC](https://arrow.apache.org/docs/format/Columnar.html#ipc-file-format) is Apache
7+
Arrow's built-in file format, formerly called Feather V2. This suite writes it without
8+
optional buffer compression. Its timings therefore isolate serialization and deserialization
9+
without codec cost. Its file size provides the approximately 1x baseline for compression
10+
ratios. Parquet provides the established reference for a compressed columnar representation.
11+
12+
The suite covers NYC taxi data and several
513
[Public BI](https://github.com/cwida/public_bi_benchmark) tables (Arade, Bimbo,
614
CMSprovider, Euro2016, Food, HashTags), TPC-H `l_comment` variants, and synthetic nested
715
data. This is the workload behind the `Compression` PR comment.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use std::fs::File;
5+
use std::io::Cursor;
6+
use std::path::Path;
7+
use std::sync::Arc;
8+
use std::time::Duration;
9+
use std::time::Instant;
10+
11+
use arrow_array::RecordBatch;
12+
use arrow_ipc::reader::FileReader;
13+
use arrow_ipc::writer::FileWriter;
14+
use arrow_schema::Schema;
15+
use async_trait::async_trait;
16+
use bytes::Bytes;
17+
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
18+
use vortex_bench::Format;
19+
use vortex_bench::compress::Compressor;
20+
use vortex_bench::compress::read_projection;
21+
22+
/// Uncompressed Arrow IPC file baseline.
23+
pub struct ArrowIpcCompressor;
24+
25+
#[async_trait]
26+
impl Compressor for ArrowIpcCompressor {
27+
fn format(&self) -> Format {
28+
Format::ArrowIpc
29+
}
30+
31+
async fn compress(&self, parquet_path: &Path) -> anyhow::Result<(u64, Duration)> {
32+
let file = File::open(parquet_path)?;
33+
let builder = ParquetRecordBatchReaderBuilder::try_new(file)?;
34+
let schema = Arc::clone(builder.schema());
35+
let batches = builder.build()?.collect::<Result<Vec<_>, _>>()?;
36+
37+
let mut buf = Vec::new();
38+
let start = Instant::now();
39+
arrow_file_write(&mut buf, &schema, &batches)?;
40+
let elapsed = start.elapsed();
41+
Ok((buf.len() as u64, elapsed))
42+
}
43+
44+
async fn decompress(&self, parquet_path: &Path) -> anyhow::Result<Duration> {
45+
let file = File::open(parquet_path)?;
46+
let builder = ParquetRecordBatchReaderBuilder::try_new(file)?;
47+
let schema = Arc::clone(builder.schema());
48+
let batches = builder.build()?.collect::<Result<Vec<_>, _>>()?;
49+
50+
let mut buf = Vec::new();
51+
arrow_file_write(&mut buf, &schema, &batches)?;
52+
53+
let start = Instant::now();
54+
arrow_file_read(Bytes::from(buf), schema.fields().len())?;
55+
Ok(start.elapsed())
56+
}
57+
}
58+
59+
#[inline(never)]
60+
fn arrow_file_write(
61+
buf: &mut Vec<u8>,
62+
schema: &Schema,
63+
batches: &[RecordBatch],
64+
) -> anyhow::Result<()> {
65+
let mut writer = FileWriter::try_new(buf, schema)?;
66+
for batch in batches {
67+
writer.write(batch)?;
68+
}
69+
writer.finish()?;
70+
Ok(())
71+
}
72+
73+
#[inline(never)]
74+
fn arrow_file_read(buf: Bytes, root_columns: usize) -> anyhow::Result<usize> {
75+
let cursor = Cursor::new(buf);
76+
let projection = read_projection(root_columns).map(<[usize]>::to_vec);
77+
let reader = FileReader::try_new(cursor, projection)?;
78+
79+
let mut nbytes = 0;
80+
for batch in reader {
81+
nbytes += batch?.get_array_memory_size();
82+
}
83+
Ok(nbytes)
84+
}

benchmarks/compress-bench/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#[cfg(feature = "lance")]
55
pub use lance_bench::compress::LanceCompressor;
6+
pub mod arrow;
67
pub mod gpu;
78
pub mod parquet;
89
pub mod vortex;

benchmarks/compress-bench/src/main.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ use anyhow::Context;
1010
use clap::Parser;
1111
#[cfg(feature = "lance")]
1212
use compress_bench::LanceCompressor;
13+
use compress_bench::arrow::ArrowIpcCompressor;
1314
use compress_bench::gpu::GpuCodec;
1415
use compress_bench::gpu::GpuOptions;
1516
use compress_bench::gpu::compressor as gpu_compressor;
1617
use compress_bench::parquet::ParquetCompressor;
18+
use compress_bench::parquet::arrow_uncompressed_size;
1719
use compress_bench::vortex::VortexCompressor;
1820
use futures::FutureExt;
1921
use indicatif::ProgressBar;
@@ -58,7 +60,7 @@ struct Args {
5860
long,
5961
value_delimiter = ',',
6062
value_enum,
61-
default_values_t = vec![Format::Parquet, Format::OnDiskVortex]
63+
default_values_t = vec![Format::ArrowIpc, Format::Parquet, Format::OnDiskVortex]
6264
)]
6365
formats: Vec<Format>,
6466
#[arg(short, long, default_value_t = 5)]
@@ -188,6 +190,7 @@ fn get_compressor(format: Format, mode: BenchMode) -> Box<dyn Compressor> {
188190
}
189191

190192
match format {
193+
Format::ArrowIpc => Box::new(ArrowIpcCompressor),
191194
Format::OnDiskVortex => Box::new(VortexCompressor),
192195
Format::Parquet => Box::new(ParquetCompressor::new()),
193196
#[cfg(feature = "lance")]
@@ -217,7 +220,11 @@ async fn run_compress(
217220
output_path: Option<PathBuf>,
218221
ingest_output: Option<PathBuf>,
219222
) -> anyhow::Result<()> {
220-
let targets = formats
223+
let timing_targets = formats
224+
.iter()
225+
.map(|f| Target::new(Engine::default(), *f))
226+
.collect_vec();
227+
let size_targets = formats
221228
.iter()
222229
.map(|f| Target::new(Engine::default(), *f))
223230
.collect_vec();
@@ -360,15 +367,15 @@ async fn run_compress(
360367
// publishes the numbers for the datasets that did decode.
361368
match display_format {
362369
DisplayFormat::Table => {
363-
render_table(&mut writer, measurements.timings, &targets)?;
370+
render_table(&mut writer, measurements.timings, &timing_targets)?;
364371
render_table(
365372
&mut writer,
366-
measurements.ratios,
367-
&if formats.contains(&Format::OnDiskVortex) {
368-
vec![Target::new(Engine::default(), Format::OnDiskVortex)]
369-
} else {
370-
vec![]
371-
},
373+
measurements
374+
.ratios
375+
.into_iter()
376+
.filter(|measurement| measurement.unit == "bytes")
377+
.collect(),
378+
&size_targets,
372379
)?;
373380
}
374381
DisplayFormat::GhJson => {
@@ -421,6 +428,11 @@ async fn run_benchmark_for_dataset(
421428

422429
// Get the parquet file path for this dataset
423430
let parquet_path = dataset_handle.to_parquet_path().await?;
431+
let uncompressed_size = ops
432+
.contains(&CompressOp::Compress)
433+
.then(|| arrow_uncompressed_size(&parquet_path))
434+
.transpose()
435+
.with_context(|| format!("measuring Arrow memory size for {bench_name}"))?;
424436

425437
let mut ratios = Vec::new();
426438
let mut timings = Vec::new();
@@ -460,6 +472,7 @@ async fn run_benchmark_for_dataset(
460472
v3_variant,
461473
*format,
462474
result.compressed_size,
475+
uncompressed_size.context("compression size requires Arrow memory size")?,
463476
));
464477
ratios.extend(result.ratios);
465478
timings.push(result.timing);

benchmarks/compress-bench/src/parquet.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,23 @@ impl Default for ParquetCompressor {
4545
}
4646
}
4747

48+
/// Return the Arrow memory size after decoding the input Parquet file.
49+
pub fn arrow_uncompressed_size(parquet_path: &Path) -> anyhow::Result<u64> {
50+
let file = File::open(parquet_path)?;
51+
let reader = ParquetRecordBatchReaderBuilder::try_new(file)?.build()?;
52+
let mut total = 0u64;
53+
54+
for batch in reader {
55+
let batch = batch?;
56+
let batch_size = u64::try_from(batch.get_array_memory_size())?;
57+
total = total
58+
.checked_add(batch_size)
59+
.ok_or_else(|| anyhow::anyhow!("Arrow memory size exceeds u64"))?;
60+
}
61+
62+
Ok(total)
63+
}
64+
4865
#[async_trait]
4966
impl Compressor for ParquetCompressor {
5067
fn format(&self) -> Format {

benchmarks/datafusion-bench/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ pub fn format_to_df_format(format: Format) -> Arc<dyn FileFormat> {
112112
Format::OnDiskVortex | Format::VortexCompact | Format::VortexSpatialNative => Arc::new(
113113
VortexFormat::new_with_options(SESSION.clone(), vortex_table_options()),
114114
),
115-
Format::OnDiskDuckDB | Format::Lance => {
115+
Format::ArrowIpc | Format::OnDiskDuckDB | Format::Lance => {
116116
unimplemented!("Format {format} cannot be turned into a DataFusion `FileFormat`")
117117
}
118118
}

benchmarks/random-access-bench/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
Measures point-lookup latency: fetching individual rows by index from a file, rather than
44
scanning it. This is the workload behind the `Random Access` PR comment.
55

6+
[Arrow IPC](https://arrow.apache.org/docs/format/Columnar.html#ipc-file-format) is Apache
7+
Arrow's built-in file format, formerly called Feather V2. This suite writes it without
8+
optional buffer compression, so it provides the established constant-time access reference.
9+
Parquet provides the established reference for a compressed columnar representation. Together,
10+
they let the suite compare Vortex and Lance against both ends of the storage trade-off.
11+
612
Two access patterns are generated with a fixed seed (see [`src/main.rs`](./src/main.rs)):
713

814
- **correlated**: several clusters of consecutive indices scattered across the dataset,
@@ -11,8 +17,8 @@ Two access patterns are generated with a fixed seed (see [`src/main.rs`](./src/m
1117
simulating lookups with no locality.
1218

1319
Each pattern runs over four datasets (`taxi`, `feature-vectors`, `nested-lists`,
14-
`nested-structs`) in Parquet, Lance, and Vortex, both with a cached open file handle and
15-
reopening the file per lookup. CI drives the full matrix via
20+
`nested-structs`) in Arrow IPC, Parquet, Lance, and Vortex. Each format uses a cached open file
21+
handle and a per-lookup reopen mode. CI drives the full matrix via
1622
[`scripts/random-access-split.py`](../../scripts/random-access-split.py).
1723

1824
## Running locally

0 commit comments

Comments
 (0)