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);

Reply via email to