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-11251-b3d793a6d4d77cc0038ff0ad3252faa615e8029a in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
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)) });
