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

github-merge-queue[bot] 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 26de48deaf perf: Optimize boolean -> string casts (#11251)
26de48deaf is described below

commit 26de48deafabfd1dd5f002a484d63c4bc38ae8a5
Author: Neil Conway <[email protected]>
AuthorDate: Fri Oct 2 15:46:46 2026 +0000

    perf: Optimize boolean -> string casts (#11251)
    
    # Which issue does this PR close?
    
    - N/A
    
    # Rationale for this change
    
    Casting boolean to a string type previously went through the generic
    `ArrayFormatter` code. We can do better because we know that all
    booleans result in one of two strings ("true" or "false").
    
    Benchmarks: (M4 Max)
    
    - Utf8, no nulls: 72.762 → 58.733 µs — 1.24× faster.
    - Utf8, ~10% nulls: 74.315 → 39.603 µs — 1.88× faster.
    - Utf8View, no nulls: 85.398 → 57.327 µs — 1.49× faster.
    - Utf8View, ~10% nulls: 90.908 → 51.298 µs — 1.77× faster.
    
    # What changes are included in this PR?
    
    * Introduce `cast_bool_to_string` and call it from the cast dispatch
    logic
    * Add benchmarks
    * Revise/extend unit tests
    
    # Are these changes tested?
    
    Yes; existing tests pass, new tests added. We don't need
    string-type-specific tests anymore.
    
    # Are there any user-facing changes?
    
    No.
    
    # AI usage
    
    Developed with Claude Code (Opus 5.5). I have reviewed and understand
    the resulting code.
---
 arrow-cast/src/cast/mod.rs    | 63 ++++++++++++++++++++++---------------------
 arrow-cast/src/cast/string.rs | 12 +++++++++
 arrow/benches/cast_kernels.rs | 13 +++++++++
 3 files changed, 57 insertions(+), 31 deletions(-)

diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs
index cb8a7ebb50..4bff879292 100644
--- a/arrow-cast/src/cast/mod.rs
+++ b/arrow-cast/src/cast/mod.rs
@@ -663,7 +663,7 @@ fn timestamp_to_date32<T: ArrowTimestampType>(
 /// Accepts [`CastOptions`] to specify cast behavior. See also [`cast()`].
 ///
 /// # Behavior
-/// * `Boolean` to `Utf8`: `true` => '1', `false` => `0`
+/// * `Boolean` to `Utf8`, `LargeUtf8`, or `Utf8View`: `true` => `"true"`, 
`false` => `"false"`
 /// * `Utf8` to `Boolean`: `true`, `yes`, `on`, `1` => `true`, `false`, `no`, 
`off`, `0` => `false`,
 ///   short variants are accepted, other strings return null or error
 /// * `Utf8` to Numeric: strings that can't be parsed to numbers return null, 
float strings
@@ -1272,9 +1272,9 @@ pub fn cast_with_options(
             Float16 => cast_bool_to_numeric::<Float16Type>(array, 
cast_options),
             Float32 => cast_bool_to_numeric::<Float32Type>(array, 
cast_options),
             Float64 => cast_bool_to_numeric::<Float64Type>(array, 
cast_options),
-            Utf8View => value_to_string_view(array, cast_options),
-            Utf8 => value_to_string::<i32>(array, cast_options),
-            LargeUtf8 => value_to_string::<i64>(array, cast_options),
+            Utf8View => cast_bool_to_string::<StringViewArray>(array),
+            Utf8 => cast_bool_to_string::<StringArray>(array),
+            LargeUtf8 => cast_bool_to_string::<LargeStringArray>(array),
             _ => Err(ArrowError::CastError(format!(
                 "Casting from {from_type} to {to_type} not supported",
             ))),
@@ -5204,33 +5204,34 @@ mod tests {
     }
 
     #[test]
-    fn test_cast_bool_to_utf8view() {
-        let array = BooleanArray::from(vec![Some(true), Some(false), None]);
-        let b = cast(&array, &DataType::Utf8View).unwrap();
-        let c = b.as_any().downcast_ref::<StringViewArray>().unwrap();
-        assert_eq!("true", c.value(0));
-        assert_eq!("false", c.value(1));
-        assert!(!c.is_valid(2));
-    }
-
-    #[test]
-    fn test_cast_bool_to_utf8() {
-        let array = BooleanArray::from(vec![Some(true), Some(false), None]);
-        let b = cast(&array, &DataType::Utf8).unwrap();
-        let c = b.as_any().downcast_ref::<StringArray>().unwrap();
-        assert_eq!("true", c.value(0));
-        assert_eq!("false", c.value(1));
-        assert!(!c.is_valid(2));
-    }
-
-    #[test]
-    fn test_cast_bool_to_large_utf8() {
-        let array = BooleanArray::from(vec![Some(true), Some(false), None]);
-        let b = cast(&array, &DataType::LargeUtf8).unwrap();
-        let c = b.as_any().downcast_ref::<LargeStringArray>().unwrap();
-        assert_eq!("true", c.value(0));
-        assert_eq!("false", c.value(1));
-        assert!(!c.is_valid(2));
+    fn test_cast_bool_to_string() {
+        for nulls in [
+            None,
+            Some(NullBuffer::from([false, false, true, true].repeat(18))),
+            Some(NullBuffer::new_null(72)),
+        ] {
+            // Include both Boolean values under nulls, an unaligned slice, 
and an empty slice.
+            let array = BooleanArray::new(BooleanBuffer::from([true, 
false].repeat(36)), nulls);
+            let expected: Vec<_> = ["true", "false"]
+                .repeat(36)
+                .into_iter()
+                .enumerate()
+                .map(|(i, value)| array.is_valid(i).then_some(value))
+                .collect();
+            for (offset, len) in [(0, 72), (3, 65), (72, 0)] {
+                let input = array.slice(offset, len);
+                for data_type in [Utf8, LargeUtf8, Utf8View] {
+                    let actual = cast(&input, &data_type).unwrap();
+                    let values: Vec<_> = match data_type {
+                        Utf8 => actual.as_string::<i32>().iter().collect(),
+                        LargeUtf8 => 
actual.as_string::<i64>().iter().collect(),
+                        Utf8View => actual.as_string_view().iter().collect(),
+                        _ => unreachable!(),
+                    };
+                    assert_eq!(values, expected[offset..offset + len]);
+                }
+            }
+        }
     }
 
     #[test]
diff --git a/arrow-cast/src/cast/string.rs b/arrow-cast/src/cast/string.rs
index 8e4ff6a930..8d6255f151 100644
--- a/arrow-cast/src/cast/string.rs
+++ b/arrow-cast/src/cast/string.rs
@@ -18,6 +18,18 @@
 use crate::cast::*;
 use arrow_buffer::NullBuffer;
 
+pub(crate) fn cast_bool_to_string<A>(array: &dyn Array) -> Result<ArrayRef, 
ArrowError>
+where
+    A: Array + FromIterator<Option<&'static str>> + 'static,
+{
+    let array: A = array
+        .as_boolean()
+        .iter()
+        .map(|v| v.map(|b| if b { "true" } else { "false" }))
+        .collect();
+    Ok(Arc::new(array))
+}
+
 pub(crate) fn value_to_string<O: OffsetSizeTrait>(
     array: &dyn Array,
     options: &CastOptions,
diff --git a/arrow/benches/cast_kernels.rs b/arrow/benches/cast_kernels.rs
index 2f895069fc..de8ebb4a91 100644
--- a/arrow/benches/cast_kernels.rs
+++ b/arrow/benches/cast_kernels.rs
@@ -261,6 +261,7 @@ 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 bool_array_no_nulls: ArrayRef = Arc::new(create_boolean_array(8192, 
0.0, 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();
@@ -327,6 +328,18 @@ fn add_benchmark(c: &mut Criterion) {
     c.bench_function("cast bool to int32 8192", |b| {
         b.iter(|| cast_array(&bool_array, DataType::Int32))
     });
+    c.bench_function("cast bool to string 8192", |b| {
+        b.iter(|| cast_array(&bool_array, DataType::Utf8))
+    });
+    c.bench_function("cast bool to string no nulls 8192", |b| {
+        b.iter(|| cast_array(&bool_array_no_nulls, DataType::Utf8))
+    });
+    c.bench_function("cast bool to string view 8192", |b| {
+        b.iter(|| cast_array(&bool_array, DataType::Utf8View))
+    });
+    c.bench_function("cast bool to string view no nulls 8192", |b| {
+        b.iter(|| cast_array(&bool_array_no_nulls, DataType::Utf8View))
+    });
     c.bench_function("cast float32 to int32 512", |b| {
         b.iter(|| cast_array(&f32_array, DataType::Int32))
     });

Reply via email to