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

Jefffrey 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 f12c5045e8 chore: Improve `decimal` benchmark coverage (#10954)
f12c5045e8 is described below

commit f12c5045e8c331bc3fc945eadea02b4bb1ca0d8c
Author: Neil Conway <[email protected]>
AuthorDate: Wed Sep 2 13:44:44 2026 -0400

    chore: Improve `decimal` benchmark coverage (#10954)
    
    # Which issue does this PR close?
    
    - N/A; motivated by the performance regression in #10850
    
    # Rationale for this change
    
    `decimal` did not have benchmark coverage for end-to-end CSV or JSON
    parsing; also, the `parse_decimal` microbenchmark had unrepresentative
    branch predictor behavior.
    
    <!--
    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.
    -->
    
    # What changes are included in this PR?
    
    * Add benchmark for parsing CSV with a decimal field
    * Add benchmark for parsing JSON with a decimal field
    * Improve the `parse_decimal` microbenchmark to generate a set of random
    strings to parse, rather than repeatedly parsing the same string.
    Repeatedly parsing the same string is not representative of real-world
    workloads; in particular, it gives the branch predictor an artificial
    boost, which can hide constructs that will poorly poorly in more
    realistic scenarios due to poor branch prediction.
    
    # Are these changes tested?
    
    Yes.
    
    # Are there any user-facing changes?
    
    No.
    
    # AI usage
    
    Developed with Claude Code, Fable 5.1. I revised and understand the
    resulting code.
---
 arrow-cast/benches/parse_decimal.rs | 60 ++++++++++++++++++++++++++++++---
 arrow-json/benches/json_reader.rs   | 25 ++++++++++++++
 arrow/benches/csv_reader.rs         | 67 +++++++++++++++++++++++++++++++++++--
 3 files changed, 146 insertions(+), 6 deletions(-)

diff --git a/arrow-cast/benches/parse_decimal.rs 
b/arrow-cast/benches/parse_decimal.rs
index bdffcb7cfc..81385627fd 100644
--- a/arrow-cast/benches/parse_decimal.rs
+++ b/arrow-cast/benches/parse_decimal.rs
@@ -20,19 +20,70 @@ use arrow_array::types::{
 };
 use arrow_cast::parse::parse_decimal;
 use criterion::*;
+use rand::rngs::StdRng;
+use rand::{RngExt, SeedableRng};
 use std::hint;
 
+/// The number of inputs parsed per iteration
+const INPUTS: usize = 1024;
+
+/// Creates a decimal string with the same shape as `shape`, but random digits.
+///
+/// Every character of `shape` other than a mantissa digit is kept as is: the
+/// sign, the decimal point and the exponent (marker, sign and digits). Digits
+/// are replaced by random digits, except that the leading zeros of the integer
+/// and fractional parts are kept, and the first digit after them is never
+/// zero, so the number of significant digits is the same as in `shape`.
+fn random_decimal(rng: &mut StdRng, shape: &str) -> String {
+    let (mantissa, exponent) = match shape.find(['e', 'E']) {
+        Some(at) => shape.split_at(at),
+        None => (shape, ""),
+    };
+    let mut out = String::with_capacity(shape.len());
+    let mut run_has_nonzero = false;
+    for c in mantissa.chars() {
+        match c {
+            '0' if !run_has_nonzero => out.push('0'),
+            '0'..='9' if !run_has_nonzero => {
+                out.push(char::from(b'1' + rng.random_range(0..9)));
+                run_has_nonzero = true;
+            }
+            '0'..='9' => out.push(char::from(b'0' + rng.random_range(0..10))),
+            '.' => {
+                run_has_nonzero = false;
+                out.push(c);
+            }
+            _ => out.push(c),
+        }
+    }
+    out.push_str(exponent);
+    out
+}
+
+/// Benchmarks parsing [`INPUTS`] random strings of the given `shape` as a
+/// decimal with the given `precision` and `scale`, reporting the throughput
+/// in elements.
 fn bench_parse<T: DecimalType>(
     c: &mut Criterion,
     name: &str,
-    decimal: &str,
+    shape: &str,
     precision: u8,
     scale: i8,
 ) {
-    let d = hint::black_box(decimal);
-    c.bench_function(name, |b| {
-        b.iter(|| parse_decimal::<T>(d, precision, scale).unwrap());
+    let mut rng = StdRng::seed_from_u64(42);
+    let inputs: Vec<String> = (0..INPUTS)
+        .map(|_| random_decimal(&mut rng, shape))
+        .collect();
+    let mut group = c.benchmark_group("parse_decimal");
+    group.throughput(Throughput::Elements(INPUTS as u64));
+    group.bench_function(name, |b| {
+        b.iter(|| {
+            for input in &inputs {
+                hint::black_box(parse_decimal::<T>(input, precision, 
scale).unwrap());
+            }
+        })
     });
+    group.finish();
 }
 
 fn criterion_benchmark(c: &mut Criterion) {
@@ -65,6 +116,7 @@ fn criterion_benchmark(c: &mut Criterion) {
     }
 
     let decimal128 = [
+        ("string decimal128 short", "1234567.89", 2),
         ("string decimal128 integer", "12345678912345678", 3),
         ("string decimal128 exact scale", "12345678912345.123", 3),
         ("string decimal128 padded scale", "12345678912345.1", 6),
diff --git a/arrow-json/benches/json_reader.rs 
b/arrow-json/benches/json_reader.rs
index 138765eebb..6b0aa7c4c0 100644
--- a/arrow-json/benches/json_reader.rs
+++ b/arrow-json/benches/json_reader.rs
@@ -22,6 +22,8 @@ use arrow_schema::{DataType, Field, Schema};
 use criterion::{
     BenchmarkId, Criterion, SamplingMode, Throughput, criterion_group, 
criterion_main,
 };
+use rand::rngs::StdRng;
+use rand::{RngExt, SeedableRng};
 use serde::Serialize;
 use serde_json::{Map, Number, Value};
 use std::fmt::Write;
@@ -405,6 +407,28 @@ fn bench_serialize_map(c: &mut Criterion) {
     bench_serialize_values(c, "decode_map_large_serialize", &large_values, 
schema);
 }
 
+fn build_decimal_json(rows: usize) -> Vec<u8> {
+    // Builds newline-delimited JSON objects with a single number field of
+    // seven integer and two fractional digits, e.g. {"d":8402913.57}
+    let mut rng = StdRng::seed_from_u64(42);
+    let mut out = String::with_capacity(rows * 18);
+    for _ in 0..rows {
+        let value: u32 = rng.random_range(100_000_000..1_000_000_000);
+        writeln!(&mut out, "{{\"d\":{}.{:02}}}", value / 100, value % 
100).unwrap();
+    }
+    out.into_bytes()
+}
+
+fn bench_decode_decimal(c: &mut Criterion) {
+    let data = build_decimal_json(ROWS);
+    let schema = Arc::new(Schema::new(vec![Field::new(
+        "d",
+        DataType::Decimal128(10, 2),
+        false,
+    )]));
+    bench_decode_schema(c, "decode_decimal128_json", &data, schema);
+}
+
 fn build_ree_json(rows: usize, run_length: usize) -> Vec<u8> {
     let mut out = String::with_capacity(rows * 24);
     for row in 0..rows {
@@ -539,6 +563,7 @@ criterion_group!(
     bench_serialize_map,
     bench_decode_ree,
     bench_serialize_ree,
+    bench_decode_decimal,
     bench_schema_inference
 );
 criterion_main!(benches);
diff --git a/arrow/benches/csv_reader.rs b/arrow/benches/csv_reader.rs
index f4085ac928..acdca07199 100644
--- a/arrow/benches/csv_reader.rs
+++ b/arrow/benches/csv_reader.rs
@@ -54,6 +54,35 @@ fn do_bench(c: &mut Criterion, name: &str, cols: 
Vec<ArrayRef>) {
     }
 }
 
+/// Creates a decimal array of `size` random values with `digits` significant
+/// digits (no leading zero) for the given `precision` and `scale`
+fn create_decimal_array<T: DecimalType>(
+    size: usize,
+    null_density: f32,
+    digits: u32,
+    precision: u8,
+    scale: i8,
+) -> ArrayRef {
+    let mut rng = seedable_rng();
+    let ten = T::Native::usize_as(10);
+    let values = (0..size).map(|_| {
+        if rng.random::<f32>() < null_density {
+            return None;
+        }
+        let mut value = T::Native::usize_as(rng.random_range(1..10));
+        for _ in 1..digits {
+            value = value
+                .mul_wrapping(ten)
+                .add_wrapping(T::Native::usize_as(rng.random_range(0..10)));
+        }
+        Some(value)
+    });
+    let array = PrimitiveArray::<T>::from_iter(values)
+        .with_precision_and_scale(precision, scale)
+        .unwrap();
+    Arc::new(array)
+}
+
 fn criterion_benchmark(c: &mut Criterion) {
     let mut rng = seedable_rng();
 
@@ -126,7 +155,7 @@ fn criterion_benchmark(c: &mut Criterion) {
     let cols = vec![Arc::new(create_string_view_array_with_len(4096, 0.5, 100, 
false)) as ArrayRef];
     do_bench(c, "4096 StringView(100, 0.5)", cols);
 
-    // Multi-Column(with String) tests
+    // Multi-Column (with String) tests
     let cols = vec![
         Arc::new(create_string_array_with_len::<i32>(4096, 0.5, 20)) as 
ArrayRef,
         Arc::new(create_string_array_with_len::<i32>(4096, 0., 30)) as 
ArrayRef,
@@ -151,7 +180,7 @@ fn criterion_benchmark(c: &mut Criterion) {
         cols,
     );
 
-    // Multi-Column(with StringView) tests
+    // Multi-Column (with StringView) tests
     let cols = vec![
         Arc::new(create_string_view_array_with_len(4096, 0.5, 20, false)) as 
ArrayRef,
         Arc::new(create_string_view_array_with_len(4096, 0., 30, false)) as 
ArrayRef,
@@ -175,6 +204,40 @@ fn criterion_benchmark(c: &mut Criterion) {
         "4096 StringView(20, 0.5), StringView(30, 0), f64(0), i64(0)",
         cols,
     );
+
+    // Single Decimal Column tests
+    let cols = vec![create_decimal_array::<Decimal32Type>(4096, 0., 9, 9, 2)];
+    do_bench(c, "4096 decimal32(9, 2) 9 digits(0)", cols);
+
+    let cols = vec![create_decimal_array::<Decimal64Type>(4096, 0., 18, 18, 
2)];
+    do_bench(c, "4096 decimal64(18, 2) 18 digits(0)", cols);
+
+    let cols = vec![create_decimal_array::<Decimal128Type>(4096, 0., 5, 10, 
2)];
+    do_bench(c, "4096 decimal128(10, 2) 5 digits(0)", cols);
+
+    let cols = vec![create_decimal_array::<Decimal128Type>(4096, 0., 10, 10, 
2)];
+    do_bench(c, "4096 decimal128(10, 2) 10 digits(0)", cols);
+
+    let cols = vec![create_decimal_array::<Decimal128Type>(4096, 0., 18, 18, 
6)];
+    do_bench(c, "4096 decimal128(18, 6) 18 digits(0)", cols);
+
+    let cols = vec![create_decimal_array::<Decimal128Type>(4096, 0., 38, 38, 
10)];
+    do_bench(c, "4096 decimal128(38, 10) 38 digits(0)", cols);
+
+    let cols = vec![create_decimal_array::<Decimal256Type>(4096, 0., 76, 76, 
10)];
+    do_bench(c, "4096 decimal256(76, 10) 76 digits(0)", cols);
+
+    // Multi-Column (with Decimal) tests
+    let cols = vec![
+        create_decimal_array::<Decimal128Type>(4096, 0., 10, 10, 2),
+        create_decimal_array::<Decimal128Type>(4096, 0., 18, 18, 6),
+        create_decimal_array::<Decimal128Type>(4096, 0., 10, 10, 1),
+    ];
+    do_bench(
+        c,
+        "4096 decimal128(10, 2), decimal128(18, 6), decimal128(10, 1)(0)",
+        cols,
+    );
 }
 
 criterion_group!(benches, criterion_benchmark);

Reply via email to