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