Skip to content

Commit fa40708

Browse files
authored
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. -->
1 parent 5b60fcb commit fa40708

1 file changed

Lines changed: 44 additions & 13 deletions

File tree

datafusion/functions/src/string/ascii.rs

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ impl ScalarUDFImpl for AsciiFunc {
9898
ScalarValue::Utf8(Some(s))
9999
| ScalarValue::LargeUtf8(Some(s))
100100
| ScalarValue::Utf8View(Some(s)) => {
101-
let result = s.chars().next().map_or(0, |c| c as i32);
101+
let result = first_char_code(&s);
102102
Ok(ColumnarValue::Scalar(ScalarValue::Int32(Some(result))))
103103
}
104104
_ => {
@@ -118,22 +118,53 @@ impl ScalarUDFImpl for AsciiFunc {
118118
}
119119
}
120120

121+
/// Returns the Unicode scalar value of the first character of `s`, or 0 when
122+
/// `s` is empty. Reads the leading byte first so the common all-ASCII case
123+
/// avoids constructing a `char` iterator and decoding a multi-byte sequence.
124+
#[inline]
125+
fn first_char_code(s: &str) -> i32 {
126+
match s.as_bytes().first() {
127+
None => 0,
128+
// ASCII byte: the codepoint equals the byte value.
129+
Some(&b) if b < 0x80 => b as i32,
130+
// Leading byte of a multi-byte sequence: decode the first char.
131+
Some(_) => s.chars().next().map_or(0, |c| c as i32),
132+
}
133+
}
134+
121135
fn calculate_ascii<'a, V>(array: &V) -> Result<ArrayRef, ArrowError>
122136
where
123137
V: StringArrayType<'a, Item = &'a str>,
124138
{
125-
let values: Vec<_> = (0..array.len())
126-
.map(|i| {
127-
if array.is_null(i) {
128-
0
129-
} else {
130-
let s = array.value(i);
131-
s.chars().next().map_or(0, |c| c as i32)
132-
}
133-
})
134-
.collect();
135-
136-
let array = Int32Array::new(values.into(), array.nulls().cloned());
139+
let len = array.len();
140+
let nulls = array.nulls().cloned();
141+
142+
// Split the null-handling out of the hot loop: when there is no null
143+
// buffer every index is valid, so we can skip the per-element null check
144+
// and use unchecked accessors.
145+
let values: Vec<i32> = match nulls {
146+
Some(ref n) => (0..len)
147+
.map(|i| {
148+
if n.is_null(i) {
149+
0
150+
} else {
151+
// SAFETY: `n.is_null(i)` was false, so `i` is a valid,
152+
// non-null index.
153+
let s = unsafe { array.value_unchecked(i) };
154+
first_char_code(s)
155+
}
156+
})
157+
.collect(),
158+
None => (0..len)
159+
.map(|i| {
160+
// SAFETY: no null buffer means every index in `0..len` is valid.
161+
let s = unsafe { array.value_unchecked(i) };
162+
first_char_code(s)
163+
})
164+
.collect(),
165+
};
166+
167+
let array = Int32Array::new(values.into(), nulls);
137168

138169
Ok(Arc::new(array))
139170
}

0 commit comments

Comments
 (0)