This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 901e69f913 parquet: Add bit-packing benchmarks (#10667)
901e69f913 is described below
commit 901e69f913bb8a1738009218898959e8f4cd7ec8
Author: Kosta Tarasov <[email protected]>
AuthorDate: Fri Aug 14 07:12:05 2026 -0400
parquet: Add bit-packing benchmarks (#10667)
# Which issue does this PR close?
<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax.
-->
- Part of #2257.
# Rationale for this change
- We need a bench to measure perf
<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.
-->
# What changes are included in this PR?
- Added a bit-packing benchmark
- It covers all encoding paths affected by bit
packing:
- PLAIN boolean encoding
- RLE boolean encoding
- DELTA_BINARY_PACKED for i32
- DELTA_BINARY_PACKED for i64
- DELTA_LENGTH_BYTE_ARRAY
- DELTA_BYTE_ARRAY
- Generic dictionary-index encoding
- Arrow’s specialized byte-array dictionary-index encoding
- Definition- and repetition-level encoding via the RLE/
bit-packed hybrid encoder
<!--
There is no need to duplicate the description in the issue here but it
is sometimes worth providing a summary of the individual changes in this
PR.
-->
# Are these changes tested?
- N/A
<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
2. Serve as another way to document the expected behavior of the code
If tests are not included in your PR, please explain why (for example,
are they covered by existing tests)?
If this PR claims a performance improvement, please include evidence
such as benchmark results.
-->
# Are there any user-facing changes?
<!--
If there are user-facing changes then we may require documentation to be
updated before approving the PR.
If there are any breaking changes to public APIs, please call them out.
-->
---
parquet/Cargo.toml | 5 +
parquet/benches/bit_packing.rs | 204 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 209 insertions(+)
diff --git a/parquet/Cargo.toml b/parquet/Cargo.toml
index 5b24235b1d..ce4212e8e3 100644
--- a/parquet/Cargo.toml
+++ b/parquet/Cargo.toml
@@ -274,6 +274,11 @@ name = "encoding"
required-features = ["experimental", "default"]
harness = false
+[[bench]]
+name = "bit_packing"
+required-features = ["experimental", "default"]
+harness = false
+
[[bench]]
name = "metadata"
harness = false
diff --git a/parquet/benches/bit_packing.rs b/parquet/benches/bit_packing.rs
new file mode 100644
index 0000000000..079928d540
--- /dev/null
+++ b/parquet/benches/bit_packing.rs
@@ -0,0 +1,204 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::hint::black_box;
+use std::sync::Arc;
+
+use arrow_array::{ArrayRef, StringArray};
+use arrow_schema::{DataType as ArrowDataType, Field, Schema};
+use criterion::{BatchSize, Criterion, Throughput, criterion_group,
criterion_main};
+use parquet::arrow::ArrowSchemaConverter;
+use parquet::arrow::arrow_writer::{ArrowRowGroupWriterFactory, compute_leaves};
+use parquet::basic::Encoding;
+use parquet::data_type::{BoolType, ByteArray, ByteArrayType, DataType,
Int32Type, Int64Type};
+use parquet::encoding::{DictEncoder, Encoder, get_encoder};
+use parquet::encodings::levels::LevelEncoder;
+use parquet::file::properties::WriterProperties;
+use parquet::file::writer::SerializedFileWriter;
+use parquet::schema::types::{ColumnDescPtr, ColumnDescriptor, ColumnPath,
Type};
+use rand::prelude::*;
+
+const NUM_VALUES: usize = 16 * 1024;
+
+fn column_desc<T: DataType>() -> ColumnDescPtr {
+ ColumnDescPtr::new(ColumnDescriptor::new(
+ Arc::new(
+ Type::primitive_type_builder("", T::get_physical_type())
+ .build()
+ .unwrap(),
+ ),
+ 0,
+ 0,
+ ColumnPath::new(vec![]),
+ ))
+}
+
+fn bench_encoding<T: DataType>(c: &mut Criterion, name: &str, values: &[T::T],
encoding: Encoding) {
+ let column_desc = column_desc::<T>();
+ let mut group = c.benchmark_group("bit_packing");
+ group.throughput(Throughput::Elements(values.len() as u64));
+ group.bench_function(name, |b| {
+ b.iter(|| {
+ let mut encoder = get_encoder::<T>(encoding,
&column_desc).unwrap();
+ encoder.put(black_box(values)).unwrap();
+ black_box(encoder.flush_buffer().unwrap());
+ });
+ });
+ group.finish();
+}
+
+/// Dictionary values are encoded separately from their indices. Populate the
+/// dictionary outside the timed section and measure the affected
RLE/bit-packed
+/// index-writing path.
+fn bench_dictionary_indices(c: &mut Criterion, values: &[i32]) {
+ let column_desc = column_desc::<Int32Type>();
+ let mut group = c.benchmark_group("bit_packing");
+ group.throughput(Throughput::Elements(values.len() as u64));
+ group.bench_function("rle_dictionary/i32/256_values", |b| {
+ b.iter_batched(
+ || {
+ let mut encoder =
DictEncoder::<Int32Type>::new(column_desc.clone());
+ encoder.put(values).unwrap();
+ encoder
+ },
+ |mut encoder| black_box(encoder.write_indices().unwrap()),
+ BatchSize::SmallInput,
+ );
+ });
+ group.finish();
+}
+
+/// Arrow byte arrays have a specialized dictionary encoder. Construct and
+/// populate a fresh column writer outside the timed section, then measure the
+/// close operation that writes its dictionary indices.
+fn bench_arrow_dictionary_indices(c: &mut Criterion, values: &[i32]) {
+ let array = Arc::new(StringArray::from_iter_values(
+ values.iter().map(|value| format!("value-{value:03}")),
+ )) as ArrayRef;
+ let field = Arc::new(Field::new("value", ArrowDataType::Utf8, false));
+ let arrow_schema = Arc::new(Schema::new(vec![field.clone()]));
+ let props = Arc::new(
+ WriterProperties::builder()
+ .set_dictionary_enabled(true)
+ .build(),
+ );
+ let parquet_schema = ArrowSchemaConverter::new()
+ .with_coerce_types(props.coerce_types())
+ .convert(&arrow_schema)
+ .unwrap();
+ let file_writer =
+ SerializedFileWriter::new(Vec::new(),
parquet_schema.root_schema_ptr(), props).unwrap();
+ let factory = ArrowRowGroupWriterFactory::new(&file_writer, arrow_schema);
+ let mut leaves = compute_leaves(&field, &array).unwrap();
+ let leaf = leaves.pop().unwrap();
+
+ let mut group = c.benchmark_group("bit_packing");
+ group.throughput(Throughput::Elements(values.len() as u64));
+ group.bench_function("rle_dictionary/byte_array_arrow/256_values", |b| {
+ b.iter_batched(
+ || {
+ let mut writer =
factory.create_column_writers(0).unwrap().pop().unwrap();
+ writer.write(&leaf).unwrap();
+ writer
+ },
+ |writer| black_box(writer.close().unwrap()),
+ BatchSize::SmallInput,
+ );
+ });
+ group.finish();
+}
+
+/// Definition and repetition levels use the same RLE/bit-packed hybrid as
+/// dictionary indices, but enter it through the streaming level API.
+fn bench_levels(c: &mut Criterion, levels: &[i16]) {
+ let mut group = c.benchmark_group("bit_packing");
+ group.throughput(Throughput::Elements(levels.len() as u64));
+ group.bench_function("rle/levels/bit_packed", |b| {
+ b.iter(|| {
+ let mut encoder = LevelEncoder::v2_streaming(3);
+ encoder.put_with_observer(black_box(levels), |_, _| {});
+ black_box(encoder.consume());
+ });
+ });
+ group.finish();
+}
+
+fn criterion_benchmark(c: &mut Criterion) {
+ let mut rng = StdRng::seed_from_u64(0);
+ let mut bools = Vec::with_capacity(NUM_VALUES);
+ let mut i32s = Vec::with_capacity(NUM_VALUES);
+ let mut i64s = Vec::with_capacity(NUM_VALUES);
+ let mut byte_arrays = Vec::with_capacity(NUM_VALUES);
+ let mut dictionary_values = Vec::with_capacity(NUM_VALUES);
+ let mut levels = Vec::with_capacity(NUM_VALUES);
+
+ for _ in 0..NUM_VALUES {
+ bools.push(rng.random::<bool>());
+ i32s.push(rng.random::<i32>());
+ // Keep deltas below 32 bits, mimicking timestamp-like data.
+ i64s.push(rng.random_range(0..1_i64 << 28));
+
+ let id = rng.random_range(0..4096_u32);
+ let suffix_len = rng.random_range(4..20);
+ byte_arrays.push(ByteArray::from(
+ format!("prefix/{id:04x}/{}", "x".repeat(suffix_len)).into_bytes(),
+ ));
+
+ dictionary_values.push(rng.random_range(0..256));
+ levels.push(rng.random_range(0..=3));
+ }
+
+ // Direct BitWriter and RLE/bit-packed hybrid users.
+ bench_encoding::<BoolType>(c, "plain/bool/bit_packed", &bools,
Encoding::PLAIN);
+ bench_encoding::<BoolType>(c, "rle/bool/bit_packed", &bools,
Encoding::RLE);
+
+ // DELTA_BINARY_PACKED writes each miniblock through BitWriter.
+ bench_encoding::<Int32Type>(
+ c,
+ "delta_binary_packed/i32/random",
+ &i32s,
+ Encoding::DELTA_BINARY_PACKED,
+ );
+ bench_encoding::<Int64Type>(
+ c,
+ "delta_binary_packed/i64/timestamp_like",
+ &i64s,
+ Encoding::DELTA_BINARY_PACKED,
+ );
+
+ // These byte-array encodings transitively use DELTA_BINARY_PACKED for
+ // lengths, prefix lengths, and suffix lengths.
+ bench_encoding::<ByteArrayType>(
+ c,
+ "delta_length_byte_array/variable_length",
+ &byte_arrays,
+ Encoding::DELTA_LENGTH_BYTE_ARRAY,
+ );
+ bench_encoding::<ByteArrayType>(
+ c,
+ "delta_byte_array/shared_prefix",
+ &byte_arrays,
+ Encoding::DELTA_BYTE_ARRAY,
+ );
+
+ bench_dictionary_indices(c, &dictionary_values);
+ bench_arrow_dictionary_indices(c, &dictionary_values);
+ bench_levels(c, &levels);
+}
+
+criterion_group!(benches, criterion_benchmark);
+criterion_main!(benches);