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 36229593fe perf: Optimize bool <-> numeric casts (#11242)
36229593fe is described below
commit 36229593fe00b03045ef5a8169333dbc8f943637
Author: Neil Conway <[email protected]>
AuthorDate: Tue Sep 29 04:49:57 2026 -0400
perf: Optimize bool <-> numeric casts (#11242)
# Which issue does this PR close?
- N/A
# Rationale for this change
Casting an array of numeric to boolean iterated over each array element,
checking for validity and then appending the new value and validity bit.
Using `BooleanArray::from_unary` is much faster, for two reasons:
1. It reuses the input's validity array instead of reconstructing it
bit-by-bit
2. Avoiding per-element null checks makes it easier for the compiler to
vectorize the loop.
Casting an array of boolean to numeric also used a similar
value-by-value loop. As before, we can do better by reusing the input's
validity bitmap. We also play a further trick: we chunk the input bits
into 64-bit words and process one chunk at a time; the fixed-size loop
helps LLVM vectorize more effectively.
Benchmarks: (M4 Max)
- Int32 → Boolean, ~10% nulls: 17.310 → 0.899 µs — 19.26× faster
- Boolean → Int32, ~10% nulls: 12.578 → 0.584 µs — 21.54× faster
# What changes are included in this PR?
* Optimize `cast_numeric_to_bool` and `cast_bool_to_numeric` as
described above
* Add unit tests
* Add benchmarks
# Are these changes tested?
Yes; new tests added.
# Are there any user-facing changes?
No.
# AI usage
Developed with Codex (Astra 6), reviewed and revised with Claude Code
(Opus 5.5). I reviewed and understand the resulting code.
---
arrow-cast/src/cast/mod.rs | 104 +++++++++++++++++++++++++++++-------------
arrow/benches/cast_kernels.rs | 8 ++++
2 files changed, 80 insertions(+), 32 deletions(-)
diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs
index 6eed69976e..96b9e4764c 100644
--- a/arrow-cast/src/cast/mod.rs
+++ b/arrow-cast/src/cast/mod.rs
@@ -2629,24 +2629,10 @@ fn cast_numeric_to_bool<FROM>(from: &dyn Array) ->
Result<ArrayRef, ArrowError>
where
FROM: ArrowPrimitiveType,
{
- numeric_to_bool_cast::<FROM>(from.as_primitive::<FROM>()).map(|to|
Arc::new(to) as ArrayRef)
-}
-
-fn numeric_to_bool_cast<T>(from: &PrimitiveArray<T>) -> Result<BooleanArray,
ArrowError>
-where
- T: ArrowPrimitiveType,
-{
- let mut b = BooleanBuilder::with_capacity(from.len());
-
- for i in 0..from.len() {
- if from.is_null(i) {
- b.append_null();
- } else {
- b.append_value(cast_num_to_bool::<T::Native>(from.value(i)));
- }
- }
-
- Ok(b.finish())
+ Ok(Arc::new(BooleanArray::from_unary(
+ from.as_primitive::<FROM>(),
+ cast_num_to_bool,
+ )))
}
/// Cast numeric types to boolean
@@ -2667,7 +2653,6 @@ fn cast_bool_to_numeric<TO>(
) -> Result<ArrayRef, ArrowError>
where
TO: ArrowPrimitiveType,
- TO::Native: num_traits::cast::NumCast,
{
Ok(Arc::new(bool_to_numeric_cast::<TO>(
from.as_any().downcast_ref::<BooleanArray>().unwrap(),
@@ -2678,20 +2663,24 @@ where
fn bool_to_numeric_cast<T>(from: &BooleanArray, _cast_options: &CastOptions)
-> PrimitiveArray<T>
where
T: ArrowPrimitiveType,
- T::Native: num_traits::NumCast,
{
- let iter = (0..from.len()).map(|i| {
- if from.is_null(i) {
- None
+ let to_numeric = |bits: u64, i: usize| {
+ if bits & (1 << i) != 0 {
+ T::Native::ONE
} else {
- single_bool_to_numeric::<T::Native>(from.value(i))
- }
- });
- // Benefit:
- // 20% performance improvement
- // Soundness:
- // The iterator is trustedLen because it comes from a Range
- unsafe { PrimitiveArray::<T>::from_trusted_len_iter(iter) }
+ T::Native::ZERO
+ }
+ };
+ // Unpack a 64-bit word at a time: the fixed-size inner loop helps the
+ // compiler vectorize the conversion.
+ let chunks = from.values().bit_chunks();
+ let mut values = Vec::with_capacity(from.len());
+ for bits in &chunks {
+ values.extend((0..64).map(|i| to_numeric(bits, i)));
+ }
+ let bits = chunks.remainder_bits();
+ values.extend((0..chunks.remainder_len()).map(|i| to_numeric(bits, i)));
+ PrimitiveArray::new(values.into(), from.nulls().cloned())
}
/// Cast single bool value to numeric value.
@@ -2872,7 +2861,7 @@ mod tests {
use crate::parse::parse_decimal;
use DataType::*;
use arrow_array::{Int64Array, RunArray, StringArray};
- use arrow_buffer::{Buffer, IntervalDayTime, NullBuffer};
+ use arrow_buffer::{BooleanBuffer, Buffer, IntervalDayTime, NullBuffer};
use arrow_buffer::{ScalarBuffer, i256};
use arrow_schema::{DataType, Field};
use chrono::NaiveDate;
@@ -5013,6 +5002,57 @@ mod tests {
assert!(!c.is_valid(2));
}
+ #[test]
+ fn test_cast_bool_numeric_sliced() {
+ for nulls in [
+ None,
+ Some(NullBuffer::from(
+ [false, true, true, false, true, true].repeat(12),
+ )),
+ Some(NullBuffer::new_null(72)),
+ ] {
+ // Include true values under nulls and slices crossing a bitmap
word boundary.
+ let booleans = BooleanArray::new(
+ BooleanBuffer::from([true, false, true, false, true,
false].repeat(12)),
+ nulls.clone(),
+ );
+ let numbers = Int32Array::new([1, 0, 1, 0, 1,
0].repeat(12).into(), nulls);
+ for data_type in [
+ Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64,
Float16, Float32, Float64,
+ ] {
+ let expected = cast(&numbers, &data_type).unwrap();
+ for offset in [0, 3, 63, 72] {
+ let input = booleans.slice(offset, 72 - offset);
+ let expected = expected.slice(offset, 72 - offset);
+ let actual = cast(&input, &data_type).unwrap();
+ assert_eq!(actual.as_ref(), expected.as_ref());
+
+ let actual = cast(expected.as_ref(), &Boolean).unwrap();
+ assert_eq!(actual.as_ref(), &input);
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn test_cast_float_to_bool_special_values() {
+ let numbers = Float64Array::from(vec![
+ 0.0,
+ -0.0,
+ 1.5,
+ -1.5,
+ f64::NAN,
+ f64::INFINITY,
+ f64::NEG_INFINITY,
+ ]);
+ let expected = BooleanArray::from(vec![false, false, true, true, true,
true, true]);
+ for data_type in [Float16, Float32, Float64] {
+ let input = cast(&numbers, &data_type).unwrap();
+ let actual = cast(input.as_ref(), &Boolean).unwrap();
+ assert_eq!(actual.as_ref(), &expected);
+ }
+ }
+
#[test]
fn test_cast_bool_to_utf8view() {
let array = BooleanArray::from(vec![Some(true), Some(false), None]);
diff --git a/arrow/benches/cast_kernels.rs b/arrow/benches/cast_kernels.rs
index 5956bdb89e..2f895069fc 100644
--- a/arrow/benches/cast_kernels.rs
+++ b/arrow/benches/cast_kernels.rs
@@ -259,6 +259,8 @@ fn cast_array(array: &ArrayRef, to_type: DataType) {
fn add_benchmark(c: &mut Criterion) {
let i32_array = build_array::<Int32Type>(512);
+ let i32_array_8192 = build_array::<Int32Type>(8192);
+ let bool_array: ArrayRef = Arc::new(create_boolean_array(8192, 0.1, 0.5));
let i64_array = build_array::<Int64Type>(512);
let f32_array = build_array::<Float32Type>(512);
let f32_utf8_array = cast(&build_array::<Float32Type>(512),
&DataType::Utf8).unwrap();
@@ -319,6 +321,12 @@ fn add_benchmark(c: &mut Criterion) {
c.bench_function("cast int32 to int64 512", |b| {
b.iter(|| cast_array(&i32_array, DataType::Int64))
});
+ c.bench_function("cast int32 to bool 8192", |b| {
+ b.iter(|| cast_array(&i32_array_8192, DataType::Boolean))
+ });
+ c.bench_function("cast bool to int32 8192", |b| {
+ b.iter(|| cast_array(&bool_array, DataType::Int32))
+ });
c.bench_function("cast float32 to int32 512", |b| {
b.iter(|| cast_array(&f32_array, DataType::Int32))
});