From 8c594889873f99d188a76325583b3fcf62c5cac7 Mon Sep 17 00:00:00 2001 From: Andy Grove Date: Sat, 25 Jul 2026 07:58:16 -0600 Subject: [PATCH] perf: avoid per-row String allocation in Spark bin and char bin formatted every row with format!("{value:b}"), and char built a String for a single character with ch.to_string(), before collecting into a StringArray. Both render into a stack buffer instead: bin writes its digits right-aligned in a [u8; 64] and char uses char::encode_utf8 into a [u8; 4]. Values are appended to a pre-sized StringBuilder rather than collected. bin -74% on small values and -50% on full-width ones, char -76%. The bin benchmark is added in #23882. --- datafusion/spark/src/function/math/bin.rs | 44 ++++++++++++++++---- datafusion/spark/src/function/string/char.rs | 4 +- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/datafusion/spark/src/function/math/bin.rs b/datafusion/spark/src/function/math/bin.rs index 82afd48e8dc9f..e6a0e1a7359ef 100644 --- a/datafusion/spark/src/function/math/bin.rs +++ b/datafusion/spark/src/function/math/bin.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use arrow::array::{ArrayRef, AsArray, StringArray}; +use arrow::array::{Array, ArrayRef, AsArray, StringBuilder}; use arrow::datatypes::{DataType, Field, FieldRef, Int64Type}; use datafusion_common::types::{NativeType, logical_int64}; use datafusion_common::utils::take_function_args; @@ -88,12 +88,20 @@ fn spark_bin_inner(arg: &[ArrayRef]) -> Result { let [array] = take_function_args("bin", arg)?; match &array.data_type() { DataType::Int64 => { - let result: StringArray = array - .as_primitive::() - .iter() - .map(|opt| opt.map(spark_bin)) - .collect(); - Ok(Arc::new(result)) + let array = array.as_primitive::(); + let len = array.len(); + // Most values are small, so 8 digits per row is a reasonable estimate; + // the buffer grows on its own for wider ones. + let mut builder = StringBuilder::with_capacity(len, len * 8); + // Digits are rendered into this stack buffer, so no row allocates. + let mut digits = [0u8; MAX_BIN_DIGITS]; + for value in array.iter() { + match value { + Some(value) => builder.append_value(spark_bin(value, &mut digits)), + None => builder.append_null(), + } + } + Ok(Arc::new(builder.finish())) } data_type => { internal_err!("bin does not support: {data_type}") @@ -101,6 +109,24 @@ fn spark_bin_inner(arg: &[ArrayRef]) -> Result { } } -fn spark_bin(value: i64) -> String { - format!("{value:b}") +/// An `i64` renders as at most 64 binary digits. +const MAX_BIN_DIGITS: usize = 64; + +/// Renders `value` as binary, right-aligned in `digits`, and returns the digits written. +/// +/// Negative values render as their two's-complement bit pattern, matching `{:b}`. +fn spark_bin(value: i64, digits: &mut [u8; MAX_BIN_DIGITS]) -> &str { + let mut pos = MAX_BIN_DIGITS; + let mut remaining = value as u64; + // `while` alone would produce an empty string for zero. + loop { + pos -= 1; + digits[pos] = b'0' + (remaining & 1) as u8; + remaining >>= 1; + if remaining == 0 { + break; + } + } + // SAFETY: every byte written above is an ASCII '0' or '1'. + unsafe { std::str::from_utf8_unchecked(&digits[pos..]) } } diff --git a/datafusion/spark/src/function/string/char.rs b/datafusion/spark/src/function/string/char.rs index 15b00ee98f5c7..5d6de3ae368e3 100644 --- a/datafusion/spark/src/function/string/char.rs +++ b/datafusion/spark/src/function/string/char.rs @@ -112,6 +112,8 @@ fn chr(args: &[ArrayRef]) -> Result { integer_array.len(), ); + // Each character encodes into this stack buffer, so no row allocates a `String`. + let mut encoded = [0u8; 4]; for integer_opt in integer_array { match integer_opt { Some(integer) => { @@ -119,7 +121,7 @@ fn chr(args: &[ArrayRef]) -> Result { builder.append_value(""); // empty string for negative numbers. } else { match core::char::from_u32((integer % 256) as u32) { - Some(ch) => builder.append_value(ch.to_string()), + Some(ch) => builder.append_value(ch.encode_utf8(&mut encoded)), None => { return exec_err!( "requested character not compatible for encoding."