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-11426-66dd257bc25f908ae4550fa080899fd194d0e052
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git

commit 45605003b8d541e008983b80d7383d5fd81bd787
Author: Matt Butrovich <[email protected]>
AuthorDate: Thu Oct 8 17:49:08 2026 +0000

    bench(arrow-cast): add decimal downscale and decimal to integer cast 
benchmarks (#11426)
    
    # Which issue does this PR close?
    
    - Part of #11421.
    
    # Rationale for this change
    
    #11421 proposes replacing the per-row division by a power of ten in
    decimal casts with a precomputed reciprocal. The kernel PRs need
    benchmarks on `main` to report before and after numbers, and the
    existing cases in `arrow/benches/cast_kernels.rs` don't cover the inputs
    where the methods differ. Their [decimal
    
arrays](https://github.com/apache/arrow-rs/blob/c7f60c612bd16ca5485cc9acddfd216c10ea9765/arrow/benches/cast_kernels.rs#L84-L114)
    hold non-negative values below 10^9, so every downscale and decimal to
    integer case divides a value of at most 9 digits.
    
    # What changes are included in this PR?
    
    This PR adds a `cast_decimal` benchmark to `arrow-cast` with three
    groups. Every array is built from a fixed seed with a chosen number of
    digits, and half of the values are negative.
    
    - `decimal_downscale` casts 8192 values to a smaller scale. It covers
    each decimal width, `Decimal128` values with 15, 18, 30 and 38 digits, a
    cast that takes the fallible path, and `Decimal256` values with 38 and
    60 digits. Three cases cast `Decimal256` to `Decimal128(38, 6)` from
    `Decimal256(41, 20)`, `Decimal256(76, 20)` and `Decimal256(76, 40)`,
    which are the result types of `Decimal256` multiplies of `Decimal128(20,
    10)`, `Decimal128(38, 10)` and `Decimal128(38, 20)` operands. Engines
    that evaluate a `Decimal128` multiply in `Decimal256`, such as Comet
    before
    
[`WideDecimalBinaryExpr`](https://github.com/apache/datafusion-comet/blob/8d0bb01a5a99924024e07c1722e6eddd9260e167/native/spark-expr/benches/wide_decimal.rs#L77-L105),
    run these casts.
    - `decimal_downscale_small` runs one `Decimal128` case and two
    `Decimal256` cases on 1, 16 and 128 values. Building a reciprocal costs
    more than one division, so these cases show where the per-array setup
    outweighs the per-value savings.
    - `decimal_to_int64` casts `Decimal64`, `Decimal128` and `Decimal256`
    values with 18 to 58 digits to `Int64`.
    
    The 8192-value cases run on an array without nulls and on the same
    values with every tenth one null. The cases that can fail also run with
    `safe: false`, because both kernels have a separate loop for it. A
    strict cast also returns an error if a value doesn't fit, so the
    benchmark can't time the failure path by mistake.
    
    # Are these changes tested?
    
    This PR only adds benchmarks.
    
    # Are there any significant user-facing changes?
    
    No.
    
    ## AI Disclosure
    
    I wrote this PR with help from Claude but understand and support the
    core approach, implementation, and test coverage.
---
 arrow-cast/Cargo.toml              |   4 +
 arrow-cast/benches/cast_decimal.rs | 262 +++++++++++++++++++++++++++++++++++++
 2 files changed, 266 insertions(+)

diff --git a/arrow-cast/Cargo.toml b/arrow-cast/Cargo.toml
index dfe10803c6..2d1364e525 100644
--- a/arrow-cast/Cargo.toml
+++ b/arrow-cast/Cargo.toml
@@ -98,6 +98,10 @@ workspace = true
 name = "format_decimal"
 harness = false
 
+[[bench]]
+name = "cast_decimal"
+harness = false
+
 [[bench]]
 name = "format_boolean"
 harness = false
diff --git a/arrow-cast/benches/cast_decimal.rs 
b/arrow-cast/benches/cast_decimal.rs
new file mode 100644
index 0000000000..79bae90d38
--- /dev/null
+++ b/arrow-cast/benches/cast_decimal.rs
@@ -0,0 +1,262 @@
+// 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.
+
+use std::hint::black_box;
+use std::sync::Arc;
+
+use arrow_array::ArrowNativeTypeOp;
+use arrow_array::types::{
+    Decimal32Type, Decimal64Type, Decimal128Type, Decimal256Type, DecimalType,
+};
+use arrow_array::{ArrayRef, PrimitiveArray};
+use arrow_buffer::{ArrowNativeType, NullBuffer};
+use arrow_cast::{CastOptions, cast_with_options};
+use arrow_schema::DataType;
+use criterion::measurement::WallTime;
+use criterion::{BenchmarkGroup, Criterion, Throughput, criterion_group, 
criterion_main};
+use rand::rngs::StdRng;
+use rand::{RngExt, SeedableRng};
+
+const ARRAY_LEN: usize = 8192;
+
+/// Creates an array of `len` random values with `digits` significant digits
+/// (no leading zero, half of them negative) for the given `precision` and 
`scale`
+fn decimal_array<T: DecimalType>(
+    len: usize,
+    digits: u32,
+    precision: u8,
+    scale: i8,
+) -> PrimitiveArray<T> {
+    let mut rng = StdRng::seed_from_u64(42);
+    let ten = T::Native::usize_as(10);
+    let values = (0..len).map(|_| {
+        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)));
+        }
+        if rng.random::<bool>() {
+            value.neg_wrapping()
+        } else {
+            value
+        }
+    });
+    PrimitiveArray::<T>::from_iter_values(values)
+        .with_precision_and_scale(precision, scale)
+        .unwrap()
+}
+
+/// Returns an array of [`ARRAY_LEN`] values from [`decimal_array`] without 
nulls,
+/// and the same values with every tenth one null
+fn inputs<T: DecimalType>(digits: u32, precision: u8, scale: i8) -> 
Vec<(&'static str, ArrayRef)> {
+    let array = decimal_array::<T>(ARRAY_LEN, digits, precision, scale);
+    let (data_type, values, _) = array.clone().into_parts();
+    let nulls = NullBuffer::from_iter((0..ARRAY_LEN).map(|i| i % 10 != 0));
+    let mixed_nulls = PrimitiveArray::<T>::new(values, 
Some(nulls)).with_data_type(data_type);
+    vec![
+        ("no_nulls", Arc::new(array)),
+        ("mixed_nulls", Arc::new(mixed_nulls)),
+    ]
+}
+
+/// Benchmarks casting each of `inputs` to `to_type`, once for each value of
+/// `CastOptions::safe` in `modes`
+fn bench_cast(
+    group: &mut BenchmarkGroup<WallTime>,
+    name: &str,
+    inputs: Vec<(&str, ArrayRef)>,
+    to_type: DataType,
+    modes: &[bool],
+) {
+    for (input, array) in inputs {
+        for &safe in modes {
+            let options = CastOptions {
+                safe,
+                ..Default::default()
+            };
+            let mode = if safe { "safe" } else { "strict" };
+            group.throughput(Throughput::Elements(array.len() as u64));
+            group.bench_function(format!("{name}/{input}/{mode}"), |b| {
+                b.iter(|| {
+                    black_box(cast_with_options(black_box(&array), &to_type, 
&options).unwrap())
+                })
+            });
+        }
+    }
+}
+
+fn downscale(c: &mut Criterion) {
+    let mut group = c.benchmark_group("decimal_downscale");
+    bench_cast(
+        &mut group,
+        "decimal32 (9, 4) -> (9, 2) 9 digits",
+        inputs::<Decimal32Type>(9, 9, 4),
+        DataType::Decimal32(9, 2),
+        &[true],
+    );
+    bench_cast(
+        &mut group,
+        "decimal64 (18, 6) -> (18, 2) 18 digits",
+        inputs::<Decimal64Type>(18, 18, 6),
+        DataType::Decimal64(18, 2),
+        &[true],
+    );
+    bench_cast(
+        &mut group,
+        "decimal128 (18, 6) -> (18, 2) 18 digits",
+        inputs::<Decimal128Type>(18, 18, 6),
+        DataType::Decimal128(18, 2),
+        &[true],
+    );
+    bench_cast(
+        &mut group,
+        "decimal128 (38, 10) -> (38, 2) 15 digits",
+        inputs::<Decimal128Type>(15, 38, 10),
+        DataType::Decimal128(38, 2),
+        &[true],
+    );
+    bench_cast(
+        &mut group,
+        "decimal128 (38, 10) -> (38, 2) 38 digits",
+        inputs::<Decimal128Type>(38, 38, 10),
+        DataType::Decimal128(38, 2),
+        &[true],
+    );
+    bench_cast(
+        &mut group,
+        "decimal128 (38, 20) -> (38, 2) 38 digits",
+        inputs::<Decimal128Type>(38, 38, 20),
+        DataType::Decimal128(38, 2),
+        &[true],
+    );
+    bench_cast(
+        &mut group,
+        "decimal128 (38, 10) -> (30, 2) 30 digits fallible",
+        inputs::<Decimal128Type>(30, 38, 10),
+        DataType::Decimal128(30, 2),
+        &[true, false],
+    );
+    bench_cast(
+        &mut group,
+        "decimal256 (76, 10) -> (76, 2) 38 digits",
+        inputs::<Decimal256Type>(38, 76, 10),
+        DataType::Decimal256(76, 2),
+        &[true],
+    );
+    bench_cast(
+        &mut group,
+        "decimal256 (76, 10) -> (76, 2) 60 digits",
+        inputs::<Decimal256Type>(60, 76, 10),
+        DataType::Decimal256(76, 2),
+        &[true],
+    );
+    // The next three are the result types of Decimal256 multiplies rescaled to
+    // Decimal128(38, 6), as when a Decimal128 multiply is evaluated in 
Decimal256
+    bench_cast(
+        &mut group,
+        "decimal256 (41, 20) -> decimal128(38, 6) 30 digits",
+        inputs::<Decimal256Type>(30, 41, 20),
+        DataType::Decimal128(38, 6),
+        &[true],
+    );
+    bench_cast(
+        &mut group,
+        "decimal256 (76, 20) -> decimal128(38, 6) 48 digits fallible",
+        inputs::<Decimal256Type>(48, 76, 20),
+        DataType::Decimal128(38, 6),
+        &[true, false],
+    );
+    bench_cast(
+        &mut group,
+        "decimal256 (76, 40) -> decimal128(38, 6) 42 digits fallible",
+        inputs::<Decimal256Type>(42, 76, 40),
+        DataType::Decimal128(38, 6),
+        &[true, false],
+    );
+    group.finish();
+}
+
+/// Small arrays, where the cost of setting up a division can outweigh the 
per-value work
+fn downscale_small(c: &mut Criterion) {
+    let mut group = c.benchmark_group("decimal_downscale_small");
+    for len in [1, 16, 128] {
+        bench_cast(
+            &mut group,
+            &format!("decimal128 (38, 20) -> (38, 2) 38 digits/{len}"),
+            vec![(
+                "no_nulls",
+                Arc::new(decimal_array::<Decimal128Type>(len, 38, 38, 20)),
+            )],
+            DataType::Decimal128(38, 2),
+            &[true],
+        );
+        bench_cast(
+            &mut group,
+            &format!("decimal256 (41, 20) -> decimal128(38, 6) 30 
digits/{len}"),
+            vec![(
+                "no_nulls",
+                Arc::new(decimal_array::<Decimal256Type>(len, 30, 41, 20)),
+            )],
+            DataType::Decimal128(38, 6),
+            &[true],
+        );
+        bench_cast(
+            &mut group,
+            &format!("decimal256 (76, 20) -> decimal128(38, 6) 48 
digits/{len}"),
+            vec![(
+                "no_nulls",
+                Arc::new(decimal_array::<Decimal256Type>(len, 48, 76, 20)),
+            )],
+            DataType::Decimal128(38, 6),
+            &[true],
+        );
+    }
+    group.finish();
+}
+
+fn to_integer(c: &mut Criterion) {
+    let mut group = c.benchmark_group("decimal_to_int64");
+    for (name, inputs) in [
+        (
+            "decimal64 (18, 6) 18 digits",
+            inputs::<Decimal64Type>(18, 18, 6),
+        ),
+        (
+            "decimal128 (38, 10) 18 digits",
+            inputs::<Decimal128Type>(18, 38, 10),
+        ),
+        (
+            "decimal128 (38, 20) 38 digits",
+            inputs::<Decimal128Type>(38, 38, 20),
+        ),
+        (
+            "decimal256 (76, 20) 38 digits",
+            inputs::<Decimal256Type>(38, 76, 20),
+        ),
+        (
+            "decimal256 (76, 40) 58 digits",
+            inputs::<Decimal256Type>(58, 76, 40),
+        ),
+    ] {
+        bench_cast(&mut group, name, inputs, DataType::Int64, &[true, false]);
+    }
+    group.finish();
+}
+
+criterion_group!(benches, downscale, downscale_small, to_integer);
+criterion_main!(benches);

Reply via email to