You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/07/15 15:46:15 UTC
(datafusion) branch gh-readonly-queue/main/pr-23588-f3939df8e4cdec7372e75f43a0dc050e4eb3771a created (now ccbf48795a)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-23588-f3939df8e4cdec7372e75f43a0dc050e4eb3771a
in repository https://gitbox.apache.org/repos/asf/datafusion.git
at ccbf48795a perf: optimize `upper` (6% faster) (#23588)
This branch includes the following new commits:
new ccbf48795a perf: optimize `upper` (6% faster) (#23588)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org
(datafusion) 01/01: perf: optimize `upper` (6% faster) (#23588)
Posted by gi...@apache.org.
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-23588-f3939df8e4cdec7372e75f43a0dc050e4eb3771a
in repository https://gitbox.apache.org/repos/asf/datafusion.git
commit ccbf48795a56f99b69606b5c6a9f6cb092c35237
Author: Andy Grove <ag...@apache.org>
AuthorDate: Wed Jul 15 09:45:56 2026 -0600
perf: optimize `upper` (6% faster) (#23588)
## Which issue does this PR close?
N/A
## Rationale for this change
Optimize existing expression.
## What changes are included in this PR?
Stream uppercase char mapping directly into the string builder in the
non-ASCII case-conversion path, eliminating the per-row String heap
allocation and copy that str::to_uppercase incurred (lowercase left
unchanged for final-sigma correctness; Utf8View left unchanged to
preserve byte-identical layout).
## Are these changes tested?
Existing tests.
Benchmark (criterion):
- upper_unicode_utf8: 7.073% faster (base 325499ns -> cand 302477ns)
- upper_unicode_large_utf8: 6.065% faster (base 325116ns -> cand
305397ns)
Full criterion output:
```text
upper_unicode_utf8 time: [302.60 µs 303.42 µs 304.22 µs]
change: [−7.2166% −7.0726% −6.9250%] (p = 0.00 < 0.05)
Performance has improved.
Found 14 outliers among 100 measurements (14.00%)
14 (14.00%) high severe
upper_unicode_large_utf8
time: [305.90 µs 306.55 µs 307.12 µs]
change: [−6.2853% −6.0652% −5.8544%] (p = 0.00 < 0.05)
Performance has improved.
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high mild
```
## Are there any user-facing changes?
No
<!--
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/Cargo.toml | 4 ++
datafusion/functions/benches/upper_unicode.rs | 90 +++++++++++++++++++++++++++
datafusion/functions/src/string/common.rs | 29 ++++++++-
3 files changed, 120 insertions(+), 3 deletions(-)
diff --git a/datafusion/functions/Cargo.toml b/datafusion/functions/Cargo.toml
index 7994028b47..04b9743224 100644
--- a/datafusion/functions/Cargo.toml
+++ b/datafusion/functions/Cargo.toml
@@ -245,6 +245,10 @@ required-features = ["string_expressions"]
[[bench]]
harness = false
name = "upper"
+
+[[bench]]
+harness = false
+name = "upper_unicode"
required-features = ["string_expressions"]
[[bench]]
diff --git a/datafusion/functions/benches/upper_unicode.rs b/datafusion/functions/benches/upper_unicode.rs
new file mode 100644
index 0000000000..2748c85e74
--- /dev/null
+++ b/datafusion/functions/benches/upper_unicode.rs
@@ -0,0 +1,90 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Benchmarks `upper` on non-ASCII input, which exercises the
+//! character-streaming case-conversion path (not the ASCII fast path).
+
+use std::hint::black_box;
+use std::sync::Arc;
+
+use arrow::array::{ArrayRef, LargeStringArray, StringArray};
+use arrow::datatypes::{DataType, Field};
+use criterion::{Criterion, criterion_group, criterion_main};
+use datafusion_common::config::ConfigOptions;
+use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, ScalarUDF};
+use datafusion_functions::string;
+
+// A pool of non-ASCII words so `is_ascii()` is false and the Unicode path runs.
+const WORDS: [&str; 8] = [
+ "café",
+ "straße",
+ "αλφα",
+ "こんにちは",
+ "münchen",
+ "naïve",
+ " órdenes",
+ "tschüß",
+];
+
+fn build_values(size: usize) -> Vec<Option<String>> {
+ (0..size)
+ .map(|i| {
+ if i % 10 == 0 {
+ None
+ } else {
+ // Concatenate a few words for a longer, mixed value.
+ let a = WORDS[i % WORDS.len()];
+ let b = WORDS[(i * 7 + 3) % WORDS.len()];
+ Some(format!("{a} {b} {a}"))
+ }
+ })
+ .collect()
+}
+
+fn invoke(func: &ScalarUDF, array: ArrayRef, dt: DataType) {
+ let len = array.len();
+ let config_options = Arc::new(ConfigOptions::default());
+ black_box(
+ func.invoke_with_args(ScalarFunctionArgs {
+ args: vec![ColumnarValue::Array(array)],
+ arg_fields: vec![Field::new("a", dt.clone(), true).into()],
+ number_rows: len,
+ return_field: Field::new("f", dt, true).into(),
+ config_options,
+ })
+ .unwrap(),
+ );
+}
+
+fn criterion_benchmark(c: &mut Criterion) {
+ let upper = string::upper();
+ let size = 4096;
+ let values = build_values(size);
+
+ let utf8: ArrayRef = Arc::new(StringArray::from(values.clone()));
+ let large: ArrayRef = Arc::new(LargeStringArray::from(values));
+
+ c.bench_function("upper_unicode_utf8", |b| {
+ b.iter(|| invoke(&upper, Arc::clone(&utf8), DataType::Utf8))
+ });
+ c.bench_function("upper_unicode_large_utf8", |b| {
+ b.iter(|| invoke(&upper, Arc::clone(&large), DataType::LargeUtf8))
+ });
+}
+
+criterion_group!(benches, criterion_benchmark);
+criterion_main!(benches);
diff --git a/datafusion/functions/src/string/common.rs b/datafusion/functions/src/string/common.rs
index b46353b609..11ebf7d3d6 100644
--- a/datafusion/functions/src/string/common.rs
+++ b/datafusion/functions/src/string/common.rs
@@ -21,7 +21,7 @@ use std::sync::Arc;
use crate::strings::{
GenericStringArrayBuilder, STRING_VIEW_INIT_BLOCK_SIZE, STRING_VIEW_MAX_BLOCK_SIZE,
- StringViewArrayBuilder, append_view,
+ StringViewArrayBuilder, StringWriter, append_view,
};
use arrow::array::{
Array, ArrayRef, AsArray, GenericStringArray, NullBufferBuilder, OffsetSizeTrait,
@@ -402,6 +402,29 @@ fn unicode_case(s: &str, lower: bool) -> String {
}
}
+/// Writes the case-converted form of `s` directly into `w`.
+///
+/// Uppercasing is a context-free character mapping, so each character is
+/// mapped and streamed straight into the output buffer, avoiding the
+/// intermediate `String` that `str::to_uppercase` allocates per row.
+///
+/// Lowercasing is *not* context-free — `str::to_lowercase` applies the
+/// special Greek final-sigma rule (Σ becomes ς at the end of a word but σ
+/// elsewhere), which a per-character mapping cannot reproduce — so it keeps
+/// using `str::to_lowercase`.
+#[inline]
+fn write_unicode_case(w: &mut impl StringWriter, s: &str, lower: bool) {
+ if lower {
+ w.write_str(&s.to_lowercase());
+ } else {
+ for c in s.chars() {
+ for upper in c.to_uppercase() {
+ w.write_char(upper);
+ }
+ }
+ }
+}
+
fn case_conversion(
args: &[ColumnarValue],
lower: bool,
@@ -532,14 +555,14 @@ fn case_conversion_array<O: OffsetSizeTrait>(
} else {
// SAFETY: `n.is_null(i)` was false in the branch above.
let s = unsafe { string_array.value_unchecked(i) };
- builder.try_append_value(&unicode_case(s, lower))?;
+ builder.try_append_with(|w| write_unicode_case(w, s, lower))?;
}
}
} else {
for i in 0..item_len {
// SAFETY: no null buffer means every index is valid.
let s = unsafe { string_array.value_unchecked(i) };
- builder.try_append_value(&unicode_case(s, lower))?;
+ builder.try_append_with(|w| write_unicode_case(w, s, lower))?;
}
}
Ok(Arc::new(builder.finish(nulls)?))
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org