This is an automated email from the ASF dual-hosted git repository.
etseidl 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 2e81b05de7 bench(parquet): cover DELTA_BYTE_ARRAY at sub-page-limit
value sizes (#10550)
2e81b05de7 is described below
commit 2e81b05de712b6d2f7bc877a037bb2dcb7b3d3c5
Author: Adrian Garcia Badaracco <[email protected]>
AuthorDate: Tue Aug 4 15:13:52 2026 -0500
bench(parquet): cover DELTA_BYTE_ARRAY at sub-page-limit value sizes
(#10550)
# Which issue does this PR close?
None. This is benchmark coverage split out of
https://github.com/apache/arrow-rs/pull/10549 so that the performance
change proposed there can be reviewed against benchmarks that already
exist on `main`.
# Rationale for this change
The existing `DELTA_BYTE_ARRAY` writer benchmarks (added in
https://github.com/apache/arrow-rs/pull/10512) write 128 values of 2 MiB
each against the default 1 MiB `data_page_size_limit`. Every value
exceeds the limit, so each one is cut onto its own data page, and each
page boundary clears the encoder's previous-value state. Every prefix
length is therefore 0.
Measured on `main`, writing that benchmark's own
`large_string_shared_prefix` data (256 MiB raw input):
| encoding | data_page_size_limit | output |
| --- | --- | --- |
| PLAIN | default (1 MiB) | 256.02 MiB |
| DELTA_BYTE_ARRAY | default (1 MiB) | 256.02 MiB |
| DELTA_BYTE_ARRAY | 4 MiB | 2.00 MiB |
At the default limit the `DELTA_BYTE_ARRAY` output is byte-for-byte what
`PLAIN` produces — the encoding is doing no work, so those benchmarks
cannot measure anything about prefix scanning. That is the known
regression https://github.com/apache/arrow-rs/issues/10489.
The new benchmarks use 1 KiB values, far below the page limit, so
roughly 1000 values share a page and the previous-value state survives
across them. That is the regime `DELTA_BYTE_ARRAY` is actually deployed
in.
# What changes are included in this PR?
Three new criterion benchmark groups in
`bench_small_delta_byte_array_writers`, each writing 8192 rows of 1 KiB
strings with both `PLAIN` (as a control/baseline) and
`DELTA_BYTE_ARRAY`:
- `small_string_shared_prefix` — values differ only in a trailing 8-byte
counter, so each prefix scan covers nearly the whole value.
- `small_string_partial_prefix` — values share their first 512 bytes and
then diverge, the realistic sorted-column case (paths, URLs, keys). Uses
a new `create_string_partial_prefix_bench_batch` helper.
- `small_string_distinct` — values differ from byte 0, so prefix
deduplication saves nothing.
No library code is touched.
# Are these changes tested?
These are benchmarks. The benchmark binary compiles, and `cargo fmt` and
`cargo clippy -p parquet --benches --all-features -- -D warnings` pass.
The benchmarks were also run, to confirm they resolve real differences
rather than noise. They were validated by measuring an actual candidate
change against them — the block-wise shared-prefix scan in
https://github.com/apache/arrow-rs/pull/10549 — on an A/B/A schedule
(baseline, branch, baseline again) so that machine drift is quantified
rather than assumed. The `plain` rows act as controls, since `PLAIN`
never calls the prefix scan. Times are means in ms, aarch64:
| bench | base (pre) | candidate change | base (post) |
| --- | --- | --- | --- |
| small_string_shared_prefix/plain (control) | 0.683 | 0.694 | 0.699 |
| small_string_shared_prefix/delta_byte_array | 2.832 | 0.682 | 2.870 |
| small_string_partial_prefix/plain (control) | 0.603 | 0.853 | 0.639 |
| small_string_partial_prefix/delta_byte_array | 1.876 | 0.703 | 1.925 |
| small_string_distinct/plain (control) | 0.454 | 0.471 | 0.474 |
| small_string_distinct/delta_byte_array | 0.680 | 0.710 | 0.699 |
The shared-prefix case resolves a 4.2x difference and the partial-prefix
case a 2.7x difference, both far above the largest control excursion.
The distinct case is flat, which is the correct outcome — there is no
prefix to scan there. One caveat: the
`small_string_partial_prefix/plain` control had a single noisy reading
(0.853 against baselines of 0.603 and 0.639), so that row's noise floor
is wider than the others; the delta effect on that bench is still
several times larger than that excursion.
The baseline columns also show that on `main` the shared-prefix case
costs 2.83 ms with `DELTA_BYTE_ARRAY` versus 0.68 ms with `PLAIN` — the
encoding is currently about 4x more expensive than `PLAIN` on exactly
the data it exists for.
# Are there any user-facing changes?
No. Benchmark-only change; no library code is touched.
---
parquet/benches/arrow_writer.rs | 79 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 78 insertions(+), 1 deletion(-)
diff --git a/parquet/benches/arrow_writer.rs b/parquet/benches/arrow_writer.rs
index c8dd7b8479..f7376b944d 100644
--- a/parquet/benches/arrow_writer.rs
+++ b/parquet/benches/arrow_writer.rs
@@ -148,6 +148,23 @@ fn create_large_string_distinct_bench_batch(size: usize,
value_size: usize) -> R
Ok(RecordBatch::try_from_iter([("col", array)])?)
}
+/// `size` rows of `value_size`-byte strings sharing their first
+/// `shared_bytes` bytes and differing thereafter — the realistic sorted-column
+/// case (paths, URLs, keys), where prefix deduplication saves part of each
+/// value rather than all or none of it.
+fn create_string_partial_prefix_bench_batch(
+ size: usize,
+ value_size: usize,
+ shared_bytes: usize,
+) -> Result<RecordBatch> {
+ let shared = "x".repeat(shared_bytes);
+ let tail = "y".repeat(value_size - shared_bytes - 8);
+ let array = Arc::new(StringArray::from_iter_values(
+ (0..size).map(|i| format!("{shared}{i:08}{tail}")),
+ )) as _;
+ Ok(RecordBatch::try_from_iter([("col", array)])?)
+}
+
fn create_string_and_binary_view_bench_batch(
size: usize,
null_density: f32,
@@ -676,6 +693,61 @@ fn bench_all_writers(c: &mut Criterion) {
}
}
+/// Writes BYTE_ARRAY columns of *small* string values with `DELTA_BYTE_ARRAY`,
+/// with `PLAIN` on the same data as a baseline.
+///
+/// Values here sit far below `data_page_size_limit`, so many share a page and
+/// the encoder's previous-value state survives across them. This is the regime
+/// `DELTA_BYTE_ARRAY` is actually deployed in, and — unlike the multi-MiB
+/// benches below — the one where the shared-prefix scan runs to real depth.
+///
+/// * `small_string_shared_prefix`: values differing only in a trailing
counter,
+/// so each scan covers nearly the whole value.
+/// * `small_string_partial_prefix`: values sharing their first half, the
+/// sorted-column case.
+/// * `small_string_distinct`: values differing from byte 0, where the scan
+/// stops immediately and prefix deduplication saves nothing.
+fn bench_small_delta_byte_array_writers(c: &mut Criterion) {
+ const ROWS: usize = 8192;
+ const VALUE_SIZE: usize = 1024;
+
+ let shared_prefix = create_large_string_shared_prefix_bench_batch(ROWS,
VALUE_SIZE).unwrap();
+ let partial_prefix =
+ create_string_partial_prefix_bench_batch(ROWS, VALUE_SIZE, VALUE_SIZE
/ 2).unwrap();
+ let distinct = create_large_string_distinct_bench_batch(ROWS,
VALUE_SIZE).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 [
+ ("small_string_shared_prefix", &shared_prefix),
+ ("small_string_partial_prefix", &partial_prefix),
+ ("small_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();
+ }
+}
+
/// Writes BYTE_ARRAY columns of large (multi-MiB) string values with
/// `DELTA_BYTE_ARRAY`, with `PLAIN` on the same data as a baseline.
///
@@ -723,5 +795,10 @@ fn bench_delta_byte_array_writers(c: &mut Criterion) {
}
}
-criterion_group!(benches, bench_all_writers, bench_delta_byte_array_writers);
+criterion_group!(
+ benches,
+ bench_all_writers,
+ bench_small_delta_byte_array_writers,
+ bench_delta_byte_array_writers
+);
criterion_main!(benches);