Skip to content

Conversation

@etseidl
Copy link
Contributor

@etseidl etseidl commented Dec 4, 2025

Which issue does this PR close?

Rationale for this change

Baseline for future improvements.

What changes are included in this PR?

Adds new benchmarks for reading and writing. Currently uses a fixed number of row groups, pages, and rows. Cycles through data types and encodings.

Are these changes tested?

N/A

Are there any user-facing changes?

No

@github-actions github-actions bot added the parquet Changes to the parquet crate label Dec 4, 2025
@etseidl
Copy link
Contributor Author

etseidl commented Dec 4, 2025

@alamb I borrowed liberally from your parquet footer code 😉

@etseidl
Copy link
Contributor Author

etseidl commented Dec 4, 2025

Example run

Details
group                                 base
-----                                 ----
read Binary(100) delta_byte_array     1.00     21.7±0.49ms        ? ?/sec
read Binary(100) delta_length         1.00     11.4±0.18ms        ? ?/sec
read Binary(100) dict                 1.00     12.3±0.16ms        ? ?/sec
read Binary(100) plain                1.00     10.6±0.28ms        ? ?/sec
read Binary(20) delta_byte_array      1.00     12.6±0.18ms        ? ?/sec
read Binary(20) delta_length          1.00      8.3±0.19ms        ? ?/sec
read Binary(20) dict                  1.00      7.5±0.20ms        ? ?/sec
read Binary(20) plain                 1.00      7.4±0.25ms        ? ?/sec
read Fixed(16) byte_stream_split      1.00      6.8±0.38ms        ? ?/sec
read Fixed(16) delta_byte_array       1.00      8.0±0.20ms        ? ?/sec
read Fixed(16) dict                   1.00  1775.6±49.86µs        ? ?/sec
read Fixed(16) plain                  1.00  1757.0±43.28µs        ? ?/sec
read Fixed(2) byte_stream_split       1.00  1770.9±30.40µs        ? ?/sec
read Fixed(2) delta_byte_array        1.00      8.3±0.15ms        ? ?/sec
read Fixed(2) dict                    1.00  1223.6±28.11µs        ? ?/sec
read Fixed(2) plain                   1.00  1229.1±25.39µs        ? ?/sec
read f32 byte_stream_split            1.00      5.2±0.16ms        ? ?/sec
read f32 dict                         1.00      4.2±0.09ms        ? ?/sec
read f32 plain                        1.00      3.1±0.27ms        ? ?/sec
read f64 byte_stream_split            1.00      9.1±0.61ms        ? ?/sec
read f64 dict                         1.00      4.4±0.06ms        ? ?/sec
read f64 plain                        1.00      3.4±0.14ms        ? ?/sec
read int32 byte_stream_split          1.00      5.1±0.14ms        ? ?/sec
read int32 delta_binary               1.00      4.4±0.08ms        ? ?/sec
read int32 dict                       1.00      4.9±0.68ms        ? ?/sec
read int32 plain                      1.00      3.1±0.17ms        ? ?/sec
read int64 byte_stream_split          1.00      9.2±0.67ms        ? ?/sec
read int64 delta_binary               1.00      5.0±0.09ms        ? ?/sec
read int64 dict                       1.00      4.4±0.06ms        ? ?/sec
read int64 plain                      1.00      3.4±0.04ms        ? ?/sec
write Binary(100) delta_byte_array    1.00     64.1±1.77ms        ? ?/sec
write Binary(100) delta_length        1.00     55.6±1.15ms        ? ?/sec
write Binary(100) dict                1.00     39.4±0.82ms        ? ?/sec
write Binary(100) plain               1.00     51.1±1.04ms        ? ?/sec
write Binary(20) delta_byte_array     1.00     31.9±0.35ms        ? ?/sec
write Binary(20) delta_length         1.00     24.7±0.71ms        ? ?/sec
write Binary(20) dict                 1.00     32.4±0.78ms        ? ?/sec
write Binary(20) plain                1.00     24.1±0.22ms        ? ?/sec
write Fixed(16) byte_stream_split     1.00     67.5±0.67ms        ? ?/sec
write Fixed(16) delta_byte_array      1.00    148.0±2.06ms        ? ?/sec
write Fixed(16) dict                  1.00     62.2±0.53ms        ? ?/sec
write Fixed(16) plain                 1.00     62.6±1.38ms        ? ?/sec
write Fixed(2) byte_stream_split      1.00     57.7±0.76ms        ? ?/sec
write Fixed(2) delta_byte_array       1.00    144.4±0.98ms        ? ?/sec
write Fixed(2) dict                   1.00     59.7±0.73ms        ? ?/sec
write Fixed(2) plain                  1.00     59.7±0.63ms        ? ?/sec
write f32 byte_stream_split           1.00     17.6±0.44ms        ? ?/sec
write f32 dict                        1.00     31.5±0.33ms        ? ?/sec
write f32 plain                       1.00     18.0±1.41ms        ? ?/sec
write f64 byte_stream_split           1.00     21.0±0.25ms        ? ?/sec
write f64 dict                        1.00     31.8±0.38ms        ? ?/sec
write f64 plain                       1.00     19.6±0.20ms        ? ?/sec
write int32 byte_stream_split         1.00     21.7±0.32ms        ? ?/sec
write int32 delta_binary              1.00     29.0±0.33ms        ? ?/sec
write int32 dict                      1.00     38.4±2.54ms        ? ?/sec
write int32 plain                     1.00     22.2±1.35ms        ? ?/sec
write int64 byte_stream_split         1.00     21.7±0.47ms        ? ?/sec
write int64 delta_binary              1.00     27.6±0.40ms        ? ?/sec
write int64 dict                      1.00     32.4±0.42ms        ? ?/sec
write int64 plain                     1.00     20.2±0.22ms        ? ?/sec

Copy link
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @etseidl -- this looks great

I had a few suggestions, but nothing I think is required to merge

I also ran some basic profiling on these benchmarks to see

samply record -- cargo bench --bench parquet_round_trip -- "write int64 dict"

And it looks like it is measuring what I would expect:

Image Image

I have a good feeling that the encoder/decoder is about to get a lot faster...


/// Creates a [`PrimitiveArray`] of a given `size` and `null_density`
/// filling it with random numbers generated using the provided `seed`.
pub fn create_primitive_array_with_seed<T>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a separate copy of these functions? Maybe we can reuse the existing functions in bench_utils.rs:

https://github.com/apache/arrow-rs/blob/f131b5469655c2a1afc3b23ce5e3f850d6a389cf/arrow/src/util/bench_util.rs#L252-L251

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, I just copied from https://github.com/alamb/parquet_footer_parsing/blob/main/src/datagen.rs 😅

I'll switch over 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I totally copy/pasted from arrow for that benchmark of course -- it all came full circle!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in d9bd421

.collect()
}

pub fn file_from_spec(spec: ParquetFileSpec, buf_size: Option<usize>) -> Bytes {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

fn read_write(c: &mut Criterion, spec: ParquetFileSpec, msg: &str) {
let f = file_from_spec(spec, None);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than passing in the buffer size, maybe the test could pass in the buffer directly (reusing it across calls) to avoid all output buffer allocations 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was thinking that too after submitting. I'll try switching it up some.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f622a3d

for rg in 0..spec.num_row_groups {
let col_writers = row_group_factory.create_column_writers(rg).unwrap();

let encoded_columns = encode_row_group(&schema, &spec, col_writers);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason to use this lower level api (rather than just writer.write to write the whole batch)?

Using writer.write would likely be less code and I think it might more closely mirror the API people actually use (though now I write this I am not sure I really know what people use)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😬 -- my own fault lol

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d783743


c.bench_function(&format!("read {msg}"), |b| {
b.iter(|| {
let record_reader = ParquetRecordBatchReaderBuilder::try_new(f.clone())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to double check that f is Bytes here (and thus this clone is cheap)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change to a more meaningful name 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f622a3d

@etseidl
Copy link
Contributor Author

etseidl commented Dec 5, 2025

Thanks @alamb. I was a bit surprised that the read and write times were so different.

I have a good feeling that the encoder/decoder is about to get a lot faster...

Hopefully. One thing to hit first is the fixed-len binary encoder. That seems to do a lot of allocations/copies that we should be able to avoid.

Comment on lines +43 to +46
// arrow::util::bench_util::create_fsb_array with a seed

/// Creates a random (but fixed-seeded) array of fixed size with a given null density and length
fn create_fsb_array_with_seed(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I just move this to bench_util.rs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure -- maybe a follow on PR

Copy link
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

Comment on lines +43 to +46
// arrow::util::bench_util::create_fsb_array with a seed

/// Creates a random (but fixed-seeded) array of fixed size with a given null density and length
fn create_fsb_array_with_seed(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure -- maybe a follow on PR

@alamb alamb merged commit c9fca0b into apache:main Dec 8, 2025
17 checks passed
@alamb
Copy link
Contributor

alamb commented Dec 8, 2025

Thanks @etseidl

@etseidl etseidl deleted the parquet_bench branch December 9, 2025 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

parquet Changes to the parquet crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add round trip benchmark for Parquet writer/reader

2 participants