Skip to content

Commit abdede8

Browse files
committed
Add ASCII fast path optimization to replace function
Add a fast path for replacing single ASCII characters with another single ASCII character, matching Rust's str::replace() optimization. This enables vectorization and avoids UTF-8 boundary checking overhead. Changes: - Added ASCII character detection in replace_into_string() - When both 'from' and 'to' are single ASCII bytes, use direct byte mapping - Updated benchmark to include single ASCII character replacement tests Optimization: - Fast path operates directly on bytes using simple map operation - Compiler can vectorize the byte-wise replacement - Avoids overhead of match_indices() pattern matching for this common case Benchmark Results (Single ASCII Character Replacement): - size=1024, str_len=32: 29.5 µs → 21.4 µs (27% faster) - size=1024, str_len=128: 73.9 µs → 23.4 µs (68% faster) - size=4096, str_len=32: 121.8 µs → 85.6 µs (30% faster) - size=4096, str_len=128: 316.9 µs → 83.8 µs (74% faster) The optimization shows exceptional 27-74% improvements, with the benefit scaling dramatically with string length. For 128-character strings, we achieve over 3x speedup by enabling vectorization and eliminating pattern matching overhead. This addresses reviewer feedback about capturing Rust's str::replace() optimization tricks for single ASCII character replacements.
1 parent c10d410 commit abdede8

File tree

2 files changed

+51
-11
lines changed

2 files changed

+51
-11
lines changed

datafusion/functions/benches/replace.rs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ fn create_args<O: OffsetSizeTrait>(
3535
size: usize,
3636
str_len: usize,
3737
force_view_types: bool,
38+
from_len: usize,
39+
to_len: usize,
3840
) -> Vec<ColumnarValue> {
3941
if force_view_types {
4042
let string_array =
4143
Arc::new(create_string_view_array_with_len(size, 0.1, str_len, false));
42-
let from_array = Arc::new(create_string_view_array_with_len(size, 0.1, 3, false));
43-
let to_array = Arc::new(create_string_view_array_with_len(size, 0.1, 5, false));
44+
let from_array = Arc::new(create_string_view_array_with_len(size, 0.1, from_len, false));
45+
let to_array = Arc::new(create_string_view_array_with_len(size, 0.1, to_len, false));
4446
vec![
4547
ColumnarValue::Array(string_array),
4648
ColumnarValue::Array(from_array),
@@ -49,8 +51,8 @@ fn create_args<O: OffsetSizeTrait>(
4951
} else {
5052
let string_array =
5153
Arc::new(create_string_array_with_len::<O>(size, 0.1, str_len));
52-
let from_array = Arc::new(create_string_array_with_len::<O>(size, 0.1, 3));
53-
let to_array = Arc::new(create_string_array_with_len::<O>(size, 0.1, 5));
54+
let from_array = Arc::new(create_string_array_with_len::<O>(size, 0.1, from_len));
55+
let to_array = Arc::new(create_string_array_with_len::<O>(size, 0.1, to_len));
5456

5557
vec![
5658
ColumnarValue::Array(string_array),
@@ -87,9 +89,21 @@ fn criterion_benchmark(c: &mut Criterion) {
8789
group.sample_size(10);
8890
group.measurement_time(Duration::from_secs(10));
8991

90-
// Small strings
92+
// ASCII single character replacement (fast path)
9193
let str_len = 32;
92-
let args = create_args::<i32>(size, str_len, true);
94+
let args = create_args::<i32>(size, str_len, false, 1, 1);
95+
group.bench_function(
96+
format!("replace_string_ascii_single [size={size}, str_len={str_len}]"),
97+
|b| {
98+
b.iter(|| {
99+
let args_cloned = args.clone();
100+
black_box(invoke_replace_with_args(args_cloned, size))
101+
})
102+
},
103+
);
104+
105+
// Multi-character strings (general path)
106+
let args = create_args::<i32>(size, str_len, true, 3, 5);
93107
group.bench_function(
94108
format!("replace_string_view [size={size}, str_len={str_len}]"),
95109
|b| {
@@ -100,7 +114,7 @@ fn criterion_benchmark(c: &mut Criterion) {
100114
},
101115
);
102116

103-
let args = create_args::<i32>(size, str_len, false);
117+
let args = create_args::<i32>(size, str_len, false, 3, 5);
104118
group.bench_function(
105119
format!("replace_string [size={size}, str_len={str_len}]"),
106120
|b| {
@@ -111,7 +125,7 @@ fn criterion_benchmark(c: &mut Criterion) {
111125
},
112126
);
113127

114-
let args = create_args::<i64>(size, str_len, false);
128+
let args = create_args::<i64>(size, str_len, false, 3, 5);
115129
group.bench_function(
116130
format!("replace_large_string [size={size}, str_len={str_len}]"),
117131
|b| {
@@ -124,7 +138,18 @@ fn criterion_benchmark(c: &mut Criterion) {
124138

125139
// Larger strings
126140
let str_len = 128;
127-
let args = create_args::<i32>(size, str_len, true);
141+
let args = create_args::<i32>(size, str_len, false, 1, 1);
142+
group.bench_function(
143+
format!("replace_string_ascii_single [size={size}, str_len={str_len}]"),
144+
|b| {
145+
b.iter(|| {
146+
let args_cloned = args.clone();
147+
black_box(invoke_replace_with_args(args_cloned, size))
148+
})
149+
},
150+
);
151+
152+
let args = create_args::<i32>(size, str_len, true, 3, 5);
128153
group.bench_function(
129154
format!("replace_string_view [size={size}, str_len={str_len}]"),
130155
|b| {
@@ -135,7 +160,7 @@ fn criterion_benchmark(c: &mut Criterion) {
135160
},
136161
);
137162

138-
let args = create_args::<i32>(size, str_len, false);
163+
let args = create_args::<i32>(size, str_len, false, 3, 5);
139164
group.bench_function(
140165
format!("replace_string [size={size}, str_len={str_len}]"),
141166
|b| {
@@ -146,7 +171,7 @@ fn criterion_benchmark(c: &mut Criterion) {
146171
},
147172
);
148173

149-
let args = create_args::<i64>(size, str_len, false);
174+
let args = create_args::<i64>(size, str_len, false, 3, 5);
150175
group.bench_function(
151176
format!("replace_large_string [size={size}, str_len={str_len}]"),
152177
|b| {

datafusion/functions/src/string/replace.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,21 @@ fn replace_into_string(buffer: &mut String, string: &str, from: &str, to: &str)
228228
return;
229229
}
230230

231+
// Fast path for replacing a single ASCII character with another single ASCII character
232+
// This matches Rust's str::replace() optimization and enables vectorization
233+
if let ([from_byte], [to_byte]) = (from.as_bytes(), to.as_bytes()) {
234+
if from_byte.is_ascii() && to_byte.is_ascii() {
235+
// SAFETY: We're replacing ASCII with ASCII, which preserves UTF-8 validity
236+
let replaced: Vec<u8> = string
237+
.as_bytes()
238+
.iter()
239+
.map(|b| if *b == *from_byte { *to_byte } else { *b })
240+
.collect();
241+
buffer.push_str(unsafe { std::str::from_utf8_unchecked(&replaced) });
242+
return;
243+
}
244+
}
245+
231246
let mut last_end = 0;
232247
for (start, _part) in string.match_indices(from) {
233248
buffer.push_str(&string[last_end..start]);

0 commit comments

Comments
 (0)