This is an automated email from the ASF dual-hosted git repository.
alamb 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 f8a57f83f0 bench(parquet): cover DELTA_BYTE_ARRAY large values in the
arrow_writer benchmark (#10512)
f8a57f83f0 is described below
commit f8a57f83f00cc6e694826bd13351092d8c8a1fc0
Author: Adrian Garcia Badaracco <[email protected]>
AuthorDate: Mon Aug 3 10:48:40 2026 -0500
bench(parquet): cover DELTA_BYTE_ARRAY large values in the arrow_writer
benchmark (#10512)
# Which issue does this PR close?
- Part of #10511
# Rationale for this change
No existing benchmark writes `DELTA_BYTE_ARRAY` through the writer: the
`arrow_writer` property matrix varies writer version, compression, bloom
filters, and CDC, all on the default encoding, and `encoding.rs` benches
encoders in isolation.
That gap matters for the large-value write path specifically: the
byte-budget sub-batching in `write_batch_internal` (#9972) measures raw
payload bytes, and page-boundary behavior interacts with the encoding's
cross-value state (#10489, #10505). None of that is visible to any
current benchmark. #10511 tracks whether making the byte budget
encoded-size-aware is worthwhile; these benchmarks are the measurement
for that question.
# What changes are included in this PR?
Two new batches and one new bench group in
`parquet/benches/arrow_writer.rs`:
- `large_string_shared_prefix`: 128 rows x 2 MiB, long common prefix
with a short distinct suffix (the case `DELTA_BYTE_ARRAY` exists for)
- `large_string_distinct`: same shape, values differing from byte 0 (the
adversarial case, prefix length ~0)
Values are sized so one value alone exceeds the default 1 MiB page
limit, the regime of #10489. Each batch runs under `plain` and
`delta_byte_array` properties (dictionary disabled), so the
delta-vs-plain gap on identical data separates inherent encoding cost
from writer overhead.
Results on an Apple M-series laptop, current `main`:
| group | `plain` | `delta_byte_array` |
| --- | --- | --- |
| `large_string_shared_prefix` | 59.0 ms (4.2 GiB/s) | 61.4 ms (4.1
GiB/s) |
| `large_string_distinct` | 37.2 ms (6.7 GiB/s) | 44.7 ms (5.6 GiB/s) |
Note these numbers shift when #10505 lands, in both directions:
shared-prefix delta gets slower in CPU time because the encoder starts
doing real prefix comparisons instead of degenerating to per-page
`PLAIN` (while the output shrinks ~128x), and distinct delta gets faster
from halving the page count. That sensitivity is the point of having the
benchmark.
# Are these changes tested?
The change is itself a benchmark; it compiles and runs under `cargo
bench -p parquet --bench arrow_writer`.
# Are there any user-facing changes?
No.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <[email protected]>
---
parquet/benches/arrow_writer.rs | 76 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 74 insertions(+), 2 deletions(-)
diff --git a/parquet/benches/arrow_writer.rs b/parquet/benches/arrow_writer.rs
index adc4dcc9ea..c8dd7b8479 100644
--- a/parquet/benches/arrow_writer.rs
+++ b/parquet/benches/arrow_writer.rs
@@ -20,7 +20,7 @@ extern crate criterion;
use criterion::{Bencher, Criterion, Throughput};
use parquet::arrow::ArrowWriter;
-use parquet::basic::{Compression, ZstdLevel};
+use parquet::basic::{Compression, Encoding, ZstdLevel};
extern crate arrow;
extern crate parquet;
@@ -123,6 +123,31 @@ fn create_large_string_bench_batch(size: usize,
value_size: usize) -> Result<Rec
Ok(RecordBatch::try_from_iter([("col", array)])?)
}
+/// `size` rows of `value_size`-byte strings sharing a long common prefix and
+/// ending in a short distinct suffix — the case `DELTA_BYTE_ARRAY` exists
+/// for: consecutive values dedup to a prefix length plus a few suffix bytes.
+fn create_large_string_shared_prefix_bench_batch(
+ size: usize,
+ value_size: usize,
+) -> Result<RecordBatch> {
+ let prefix = "x".repeat(value_size - 8);
+ let array = Arc::new(StringArray::from_iter_values(
+ (0..size).map(|i| format!("{prefix}{i:08}")),
+ )) as _;
+ Ok(RecordBatch::try_from_iter([("col", array)])?)
+}
+
+/// `size` rows of `value_size`-byte strings whose leading bytes differ — the
+/// adversarial case for `DELTA_BYTE_ARRAY`, where every prefix length is ~0
+/// and the encoding stores each value in full.
+fn create_large_string_distinct_bench_batch(size: usize, value_size: usize) ->
Result<RecordBatch> {
+ let filler = "x".repeat(value_size - 8);
+ let array = Arc::new(StringArray::from_iter_values(
+ (0..size).map(|i| format!("{i:08}{filler}")),
+ )) as _;
+ Ok(RecordBatch::try_from_iter([("col", array)])?)
+}
+
fn create_string_and_binary_view_bench_batch(
size: usize,
null_density: f32,
@@ -651,5 +676,52 @@ fn bench_all_writers(c: &mut Criterion) {
}
}
-criterion_group!(benches, bench_all_writers);
+/// Writes BYTE_ARRAY columns of large (multi-MiB) string values with
+/// `DELTA_BYTE_ARRAY`, with `PLAIN` on the same data as a baseline.
+///
+/// Two data shapes bracket the encoding's best and worst case:
+/// * `large_string_shared_prefix`: values like `xxx…x00000000`,
+/// `xxx…x00000001`, … share a long common prefix and differ only in a
+/// short suffix — the case `DELTA_BYTE_ARRAY` is designed to handle well,
+/// encoding each value as a prefix length plus a few suffix bytes.
+/// * `large_string_distinct`: values like `00000000x…xxx`, `00000001x…xxx`, …
+/// differ in their leading bytes, so prefix deduplication saves nothing and
+/// the encoding stores each value in full.
+fn bench_delta_byte_array_writers(c: &mut Criterion) {
+ // Each 2 MiB value alone exceeds the default 1 MiB data page size limit.
+ let shared = create_large_string_shared_prefix_bench_batch(128, 2 * 1024 *
1024).unwrap();
+ let distinct = create_large_string_distinct_bench_batch(128, 2 * 1024 *
1024).unwrap();
+
+ let plain = WriterProperties::builder()
+ .set_dictionary_enabled(false)
+ .set_encoding(Encoding::PLAIN)
+ .build();
+ let delta = WriterProperties::builder()
+ .set_dictionary_enabled(false)
+ .set_encoding(Encoding::DELTA_BYTE_ARRAY)
+ .build();
+
+ for (batch_name, batch) in [
+ ("large_string_shared_prefix", &shared),
+ ("large_string_distinct", &distinct),
+ ] {
+ let mut group = c.benchmark_group(batch_name);
+ group.throughput(Throughput::Bytes(
+ batch
+ .columns()
+ .iter()
+ .map(|f| f.get_array_memory_size() as u64)
+ .sum(),
+ ));
+
+ for (prop_name, prop) in [("plain", &plain), ("delta_byte_array",
&delta)] {
+ group.bench_function(prop_name, |b| {
+ write_batch_with_option(b, batch,
Some((*prop).clone())).unwrap()
+ });
+ }
+ group.finish();
+ }
+}
+
+criterion_group!(benches, bench_all_writers, bench_delta_byte_array_writers);
criterion_main!(benches);