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