This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-11417-2d17a72e2340175405d7a89ba4d22d0cc2e2c434
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git

commit 9d1a049a4b28c7e5bb974c69fc6b6063ddcaa424
Author: RIchard Baah <[email protected]>
AuthorDate: Wed Oct 7 14:08:10 2026 +0000

    Parquet dict string encoding bench (#11417)
    
    # Which issue does this PR close?
    
    <!--
    It is not necessary to raise an issue, but it is encouraged if it is a
    non-trivial
    enhancement/bug.
    
    If this PR relates to an existing issue, but does not fully resolve it,
    please change
    the wording below from "Closes #NNN" to "Relates to #NNN" or "Part of
    #NNN" to avoid
    false closing an issue.
    -->
    
    - related but does not complete
    https://github.com/apache/arrow-rs/issues/11414
    - related https://github.com/apache/datafusion/issues/24111
    - related
    https://github.com/apache/datafusion/issues/24117#issuecomment-5671192732
    
    # Rationale for this change
    
    see attached issue
    
    <!--
    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.
    
    Please try to explain the problem you are trying to solve in terms of
    the user-visible
    behavior, rather than the implementation.
    -->
    
    # What changes are included in this PR?
    adds new benchmarks for reading RLE_DICTIONARY into different arrow
    arrays. the focus being that reads should scale with cardinality
    <!--
    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?
    
    yes. ran the benchmarks locally 😅
    
    <!--
    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 so identify these tests)?
    
    If this PR claims a performance improvement, please include evidence
    such as
    benchmark results.
    
    DO NOT simply paste a list of commands ran to verify the code; this is
    what our
    CI is for and it pollutes the PR. ONLY explain if testing was included
    with this
    PR, or why it wasn't.
    -->
    
    # Are there any significant user-facing changes?
    
    no
    
    <!--
    Significant user-facing changes include:
    - Breaking public API change
    - Significant change to existing functionality (e.g. function returns
    something different)
    
    These need to be explicitly called out to ensure we have proper
    documentation if
    required and to ensure we follow proper semantic versioning.
    
    DO NOT restate the changes of this PR; if a bug was fixed and already
    explained
    above, we don't need this to be restated as a user-facing change.
    -->
---
 parquet/Cargo.toml                              |   5 +
 parquet/benches/arrow_reader_string_encoding.rs | 167 ++++++++++++++++++++++++
 2 files changed, 172 insertions(+)

diff --git a/parquet/Cargo.toml b/parquet/Cargo.toml
index 52346e7134..31ca245952 100644
--- a/parquet/Cargo.toml
+++ b/parquet/Cargo.toml
@@ -269,6 +269,11 @@ name = "arrow_reader_row_filter"
 required-features = ["arrow", "async"]
 harness = false
 
+[[bench]]
+name = "arrow_reader_string_encoding"
+required-features = ["arrow"]
+harness = false
+
 [[bench]]
 name = "arrow_reader_predicate_fusion"
 required-features = ["arrow", "async", "snap"]
diff --git a/parquet/benches/arrow_reader_string_encoding.rs 
b/parquet/benches/arrow_reader_string_encoding.rs
new file mode 100644
index 0000000000..a7b17d7cfc
--- /dev/null
+++ b/parquet/benches/arrow_reader_string_encoding.rs
@@ -0,0 +1,167 @@
+// 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.
+
+//! Benchmark comparing reading the SAME dictionary-encoded parquet string 
column
+//! back as two different Arrow logical types:
+//!   1. `Dictionary(Int32, Utf8)`
+//!   2. `Utf8View`
+//!
+//! Sweeps cardinality in {10, 100, 1000, 20_000} and row count
+//! in {1M, 5M}, writing a dictionary-encoded parquet file in-memory
+//! and measuring only the read time via `ArrowReaderOptions::with_schema`.
+
+use std::sync::Arc;
+
+use arrow_array::{ArrayRef, RecordBatch, StringArray};
+use arrow_schema::{DataType, Field, Schema, SchemaRef};
+use bytes::Bytes;
+use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
+use parquet::arrow::ArrowWriter;
+use parquet::arrow::arrow_reader::{ArrowReaderOptions, 
ParquetRecordBatchReaderBuilder};
+use parquet::basic::Compression;
+use parquet::file::properties::WriterProperties;
+use rand::{RngExt, SeedableRng, rngs::StdRng};
+
+/// Lengths chosen to exercise both the inlined Utf8View path (<= 12 bytes) and
+/// the out-of-line buffer path (> 12 bytes).
+const STRING_LENS: &[usize] = &[8, 24];
+const SEED: u64 = 0xC0FFEE_u64;
+const CARDINALITIES: &[usize] = &[10, 100, 1000, 20_000];
+const ROW_COUNTS: &[usize] = &[1_000_000, 5_000_000];
+const NULL_FRACTIONS: &[f64] = &[0.0, 0.5];
+const NULL_PROB_DENOM: u32 = 1_000_000;
+const BATCH_SIZE: usize = 8192;
+
+fn build_value_pool(cardinality: usize, string_len: usize) -> Vec<String> {
+    (0..cardinality)
+        .map(|idx| format!("{idx:0string_len$}"))
+        .collect()
+}
+
+fn make_string_array(
+    cardinality: usize,
+    num_rows: usize,
+    null_fraction: f64,
+    string_len: usize,
+) -> StringArray {
+    let dictionary = build_value_pool(cardinality, string_len);
+    debug_assert_eq!(dictionary[0].len(), string_len);
+    let mut rng = StdRng::seed_from_u64(
+        SEED ^ cardinality as u64 ^ num_rows as u64 ^ null_fraction.to_bits() 
^ string_len as u64,
+    );
+    let null_threshold = (null_fraction * NULL_PROB_DENOM as f64) as u32;
+    let values: Vec<Option<&str>> = (0..num_rows)
+        .map(|_| {
+            if null_fraction > 0.0 && rng.random_range(0..NULL_PROB_DENOM) < 
null_threshold {
+                None
+            } else {
+                let pick = rng.random_range(0..cardinality);
+                Some(dictionary[pick].as_str())
+            }
+        })
+        .collect();
+    StringArray::from(values)
+}
+
+fn write_parquet(column: StringArray) -> Bytes {
+    let schema: SchemaRef = Arc::new(Schema::new(vec![Field::new("value", 
DataType::Utf8, true)]));
+    let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(column) as 
ArrayRef]).unwrap();
+
+    let props = WriterProperties::builder()
+        .set_dictionary_enabled(true)
+        .set_compression(Compression::UNCOMPRESSED)
+        .build();
+
+    let mut buffer: Vec<u8> = Vec::with_capacity(64 * 1024 * 1024);
+    let mut writer = ArrowWriter::try_new(&mut buffer, schema, 
Some(props)).unwrap();
+    writer.write(&batch).unwrap();
+    writer.close().unwrap();
+    Bytes::from(buffer)
+}
+
+fn override_schema(data_type: DataType) -> SchemaRef {
+    Arc::new(Schema::new(vec![Field::new("value", data_type, true)]))
+}
+
+fn read_as(file_bytes: &Bytes, schema: SchemaRef) -> usize {
+    let options = ArrowReaderOptions::new().with_schema(schema);
+    let reader = 
ParquetRecordBatchReaderBuilder::try_new_with_options(file_bytes.clone(), 
options)
+        .unwrap()
+        .with_batch_size(BATCH_SIZE)
+        .build()
+        .unwrap();
+    let mut total_rows = 0;
+    for maybe_batch in reader {
+        let batch = maybe_batch.unwrap();
+        total_rows += batch.num_rows();
+    }
+    total_rows
+}
+
+fn criterion_benchmark(criterion: &mut Criterion) {
+    let dict_schema = override_schema(DataType::Dictionary(
+        Box::new(DataType::Int32),
+        Box::new(DataType::Utf8),
+    ));
+    let utf8_view_schema = override_schema(DataType::Utf8View);
+
+    for &num_rows in ROW_COUNTS {
+        for &cardinality in CARDINALITIES {
+            for &null_fraction in NULL_FRACTIONS {
+                for &string_len in STRING_LENS {
+                    let column =
+                        make_string_array(cardinality, num_rows, 
null_fraction, string_len);
+                    let file_bytes = write_parquet(column);
+
+                    let mut group = 
criterion.benchmark_group("RLE_DICTIONARY_read");
+                    group.sample_size(10);
+                    group.throughput(criterion::Throughput::Elements(num_rows 
as u64));
+
+                    let param = format!(
+                        
"rows={num_rows}/card={cardinality}/nulls={null_fraction:.2}/len={string_len}"
+                    );
+
+                    group.bench_with_input(
+                        BenchmarkId::new("Dictionary(Int32,Utf8)", &param),
+                        &param,
+                        |bencher, _| {
+                            bencher.iter(|| {
+                                let rows = read_as(&file_bytes, 
dict_schema.clone());
+                                assert_eq!(rows, num_rows);
+                            });
+                        },
+                    );
+                    group.bench_with_input(
+                        BenchmarkId::new("Utf8View", &param),
+                        &param,
+                        |bencher, _| {
+                            bencher.iter(|| {
+                                let rows = read_as(&file_bytes, 
utf8_view_schema.clone());
+                                assert_eq!(rows, num_rows);
+                            });
+                        },
+                    );
+
+                    group.finish();
+                }
+            }
+        }
+    }
+}
+
+criterion_group!(benches, criterion_benchmark);
+criterion_main!(benches);

Reply via email to