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-23462-5b60fcb1107b4d9b4dffa11ff10ed8083b5e3a66 in repository https://gitbox.apache.org/repos/asf/datafusion.git
commit fa40708411f59bed7a0ce9dbf34306a63b660d9a Author: Andy Grove <[email protected]> AuthorDate: Sat Jul 11 00:37:08 2026 -0600 perf: optimize ascii in datafusion-functions (#23462) ## Which issue does this PR close? N/A ## Rationale for this change Optimize existing expression. ## What changes are included in this PR? Rewrote calculate_ascii to skip per-element null checks in the no-null case, use value_unchecked to avoid bounds checks, and add an ASCII leading-byte fast path that avoids char-iterator decoding for ASCII strings. ## Are these changes tested? Existing tests. Benchmark (criterion): Wins: - ascii_string_ascii_only (null_density=0.5): 23.96% faster (base 8224ns -> cand 6253ns) - ascii_string_utf8 (null_density=0.5): 17.558% faster (base 10529ns -> cand 8680ns) - ascii_string_ascii_only (null_density=0): 35.842% faster (base 6577ns -> cand 4219ns) - ascii_string_utf8 (null_density=0): 23.205% faster (base 11179ns -> cand 8585ns) - ascii_string_view_utf8 (null_density=0): 20.23% faster (base 11948ns -> cand 9531ns) - ascii_string_view_ascii_only (null_density=0): 24.794% faster (base 7063ns -> cand 5312ns) - ascii_string_view_ascii_only (null_density=0.5): 13.057% faster (base 9270ns -> cand 8060ns) - ascii_string_view_utf8 (null_density=0.5): 9.403% faster (base 11671ns -> cand 10573ns) Within noise: - ascii_scalar_utf8view: -1.162% faster (base 44ns -> cand 44ns) - ascii_scalar_utf8: -2.266% faster (base 43ns -> cand 44ns) ## Are there any user-facing changes? <!-- If there are user-facing changes then we may require documentation to be updated before approving the PR. --> <!-- If there are any breaking changes to public APIs, please add the `api change` label. --> --- datafusion/functions/src/string/ascii.rs | 57 ++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/datafusion/functions/src/string/ascii.rs b/datafusion/functions/src/string/ascii.rs index bb5a8d0125..4447d1f174 100644 --- a/datafusion/functions/src/string/ascii.rs +++ b/datafusion/functions/src/string/ascii.rs @@ -98,7 +98,7 @@ impl ScalarUDFImpl for AsciiFunc { ScalarValue::Utf8(Some(s)) | ScalarValue::LargeUtf8(Some(s)) | ScalarValue::Utf8View(Some(s)) => { - let result = s.chars().next().map_or(0, |c| c as i32); + let result = first_char_code(&s); Ok(ColumnarValue::Scalar(ScalarValue::Int32(Some(result)))) } _ => { @@ -118,22 +118,53 @@ impl ScalarUDFImpl for AsciiFunc { } } +/// Returns the Unicode scalar value of the first character of `s`, or 0 when +/// `s` is empty. Reads the leading byte first so the common all-ASCII case +/// avoids constructing a `char` iterator and decoding a multi-byte sequence. +#[inline] +fn first_char_code(s: &str) -> i32 { + match s.as_bytes().first() { + None => 0, + // ASCII byte: the codepoint equals the byte value. + Some(&b) if b < 0x80 => b as i32, + // Leading byte of a multi-byte sequence: decode the first char. + Some(_) => s.chars().next().map_or(0, |c| c as i32), + } +} + fn calculate_ascii<'a, V>(array: &V) -> Result<ArrayRef, ArrowError> where V: StringArrayType<'a, Item = &'a str>, { - let values: Vec<_> = (0..array.len()) - .map(|i| { - if array.is_null(i) { - 0 - } else { - let s = array.value(i); - s.chars().next().map_or(0, |c| c as i32) - } - }) - .collect(); - - let array = Int32Array::new(values.into(), array.nulls().cloned()); + let len = array.len(); + let nulls = array.nulls().cloned(); + + // Split the null-handling out of the hot loop: when there is no null + // buffer every index is valid, so we can skip the per-element null check + // and use unchecked accessors. + let values: Vec<i32> = match nulls { + Some(ref n) => (0..len) + .map(|i| { + if n.is_null(i) { + 0 + } else { + // SAFETY: `n.is_null(i)` was false, so `i` is a valid, + // non-null index. + let s = unsafe { array.value_unchecked(i) }; + first_char_code(s) + } + }) + .collect(), + None => (0..len) + .map(|i| { + // SAFETY: no null buffer means every index in `0..len` is valid. + let s = unsafe { array.value_unchecked(i) }; + first_char_code(s) + }) + .collect(), + }; + + let array = Int32Array::new(values.into(), nulls); Ok(Arc::new(array)) } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
