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 f271113651 bench(parquet): cover large dictionary values in 
arrow_reader (#10691)
f271113651 is described below

commit f271113651537eccf42fd67edaae288c6461c2e3
Author: AarryaSaraf <[email protected]>
AuthorDate: Wed Aug 19 13:03:59 2026 -0700

    bench(parquet): cover large dictionary values in arrow_reader (#10691)
    
    # Which issue does this PR close?
    
    None. This is benchmark coverage split out of #10690, so that the change
    proposed there can be measured against a benchmark that already exists
    on `main`.
    It covers the shape reported in #10694.
    
    # Rationale for this change
    
    Every dictionary-encoded case in `arrow_reader` decodes ~20 byte values:
    `build_dictionary_encoded_string_page_iterator` builds `"Dictionary
    value {x}"`
    at 1% unique. Two properties of that shape keep the dictionary gather
    out of the
    measurement entirely — values are small enough that per-key overhead
    dominates
    (RLE index decoding, the per-key bounds check), and the dictionary is a
    couple of
    KiB, so it stays cached for the whole decode.
    
    Columns of large binary payloads invert both. A writer's dictionary size
    limit is
    checked lazily, so such a column is often dictionary encoded all the way
    to the
    end, with the dictionary page holding the entire column and each entry
    referenced
    exactly once. The gather then gets its data from a source far too large
    to cache,
    and it is the gather rather than the per-key work that dominates.
    
    No benchmark in the crate covers that, so changes to
    `OffsetBuffer::extend_from_dictionary` currently have nothing to be
    measured
    against.
    
    # What changes are included in this PR?
    
    One generator and one case in the existing `BinaryArray` group:
    
    - `build_dictionary_encoded_large_value_page_iterator` — 64 KiB values,
    all
    distinct, 128 per page, mandatory (no NULLs), otherwise the same
    row-group and
      page geometry as the existing generators.
    - `arrow_array_reader/BinaryArray/dictionary encoded, mandatory, no
    NULLs, large values`
    
    One iteration decodes 64 MiB of output from a 32 MiB dictionary.
    Measured on
    `main` (Apple M-series):
    
    ```
    arrow_array_reader/BinaryArray/dictionary encoded, mandatory, no NULLs, 
large values
                            time:   [7.6857 ms 7.8982 ms 8.1303 ms]
    ```
    
    # Are these changes tested?
    
    This is a benchmark. It asserts its decoded value count on each run, as
    the
    surrounding cases do.
    
    # Are there any user-facing changes?
    
    No.
    
    # AI disclosure
    
    This benchmark was drafted with AI assistance and reviewed by me.
---
 parquet/benches/arrow_reader.rs | 90 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 90 insertions(+)

diff --git a/parquet/benches/arrow_reader.rs b/parquet/benches/arrow_reader.rs
index b86306ab8f..1d30076903 100644
--- a/parquet/benches/arrow_reader.rs
+++ b/parquet/benches/arrow_reader.rs
@@ -128,6 +128,18 @@ const BATCH_SIZE: usize = 8192;
 const MAX_LIST_LEN: usize = 10;
 const EXPECTED_VALUE_COUNT: usize = NUM_ROW_GROUPS * PAGES_PER_GROUP * 
VALUES_PER_PAGE;
 
+// Params for the large dictionary value benchmark. Binary columns holding 
large
+// payloads are commonly dictionary encoded in practice, because a writer's
+// dictionary size limit is checked lazily and so is never reached before the
+// column ends. Values there are distinct, which means the dictionary page 
holds
+// the whole column and each entry is referenced exactly once: the gather reads
+// from a source far too large to stay cached, unlike the small-value cases 
above
+// whose dictionary is a few KiB. Values are correspondingly larger and fewer 
per
+// page, keeping one iteration to 64 MiB of output over a 32 MiB dictionary.
+const LARGE_VALUE_LEN: usize = 64 * 1024;
+const LARGE_VALUES_PER_PAGE: usize = 128;
+const EXPECTED_LARGE_VALUE_COUNT: usize = NUM_ROW_GROUPS * PAGES_PER_GROUP * 
LARGE_VALUES_PER_PAGE;
+
 pub fn seedable_rng() -> StdRng {
     StdRng::seed_from_u64(42)
 }
@@ -669,6 +681,67 @@ fn build_dictionary_encoded_string_page_iterator(
     InMemoryPageIterator::new(pages)
 }
 
+/// Builds pages of dictionary encoded values that are individually large and 
all
+/// distinct, to cover the cost of gathering the dictionary values into the 
output
+/// buffer. The small-value generator above is dominated by per-key overhead
+/// instead, and its dictionary is small enough to stay cached throughout.
+fn build_dictionary_encoded_large_value_page_iterator(
+    column_desc: ColumnDescPtr,
+) -> impl PageIterator + Clone {
+    use parquet::encoding::{DictEncoder, Encoder};
+    let max_def_level = column_desc.max_def_level();
+    let max_rep_level = column_desc.max_rep_level();
+    let rep_levels = vec![0; LARGE_VALUES_PER_PAGE];
+    let def_levels = vec![max_def_level; LARGE_VALUES_PER_PAGE];
+    // Every value is distinct, so the dictionary holds one entry per row and 
each
+    // entry is referenced exactly once, as it is for a column of large unique
+    // payloads. The leading bytes make each value unique; the rest is filler.
+    let make_value = |index: usize| {
+        let mut value = vec![(index % 251) as u8; LARGE_VALUE_LEN];
+        value[..8].copy_from_slice(&(index as u64).to_le_bytes());
+        value
+    };
+    let mut next_value = 0;
+    let mut pages: Vec<Vec<parquet::column::page::Page>> = Vec::new();
+    for _i in 0..NUM_ROW_GROUPS {
+        let mut column_chunk_pages = VecDeque::new();
+        let mut dict_encoder = 
DictEncoder::<ByteArrayType>::new(column_desc.clone());
+        // add data pages
+        for _j in 0..PAGES_PER_GROUP {
+            let values = (0..LARGE_VALUES_PER_PAGE)
+                .map(|_| {
+                    next_value += 1;
+                    parquet::data_type::ByteArray::from(make_value(next_value 
- 1))
+                })
+                .collect::<Vec<_>>();
+            let mut page_builder =
+                DataPageBuilderImpl::new(column_desc.clone(), values.len() as 
u32, true);
+            page_builder.add_rep_levels(max_rep_level, &rep_levels);
+            page_builder.add_def_levels(max_def_level, &def_levels);
+            let _ = dict_encoder.put(&values);
+            let indices = dict_encoder
+                .write_indices()
+                .expect("write_indices() should be OK");
+            page_builder.add_indices(indices);
+            column_chunk_pages.push_back(page_builder.consume());
+        }
+        // add dictionary page
+        let dict = dict_encoder
+            .write_dict()
+            .expect("write_dict() should be OK");
+        let dict_page = parquet::column::page::Page::DictionaryPage {
+            buf: dict,
+            num_values: dict_encoder.num_entries() as u32,
+            encoding: Encoding::RLE_DICTIONARY,
+            is_sorted: false,
+        };
+        column_chunk_pages.push_front(dict_page);
+        pages.push(column_chunk_pages.into());
+    }
+
+    InMemoryPageIterator::new(pages)
+}
+
 fn build_string_list_page_iterator(
     column_desc: ColumnDescPtr,
     null_density: f32,
@@ -2277,6 +2350,23 @@ fn add_benches(c: &mut Criterion) {
         assert_eq!(count, EXPECTED_VALUE_COUNT);
     });
 
+    // byte array, dictionary encoded, large values, no NULLs
+    let dictionary_large_value_data =
+        
build_dictionary_encoded_large_value_page_iterator(mandatory_binary_column_desc.clone());
+    group.bench_function(
+        "dictionary encoded, mandatory, no NULLs, large values",
+        |b| {
+            b.iter(|| {
+                let array_reader = create_byte_array_reader(
+                    dictionary_large_value_data.clone(),
+                    mandatory_binary_column_desc.clone(),
+                );
+                count = bench_array_reader(array_reader);
+            });
+            assert_eq!(count, EXPECTED_LARGE_VALUE_COUNT);
+        },
+    );
+
     group.finish();
 
     // binary view benchmarks

Reply via email to