Skip to content

Commit c10d410

Browse files
committed
perf: improve performance of string replace (17-32% faster)
Use a reusable String buffer instead of allocating a new String for each row. This optimization achieves 17-32% performance improvement across different string types and sizes by avoiding per-row allocations. Benchmark results: | Benchmark | Array Size | String Length | Baseline (µs) | Optimized (µs) | Improvement | |---------------|------------|---------------|---------------|----------------|-------------| | string_view | 1024 | 32 | 32.53 | 22.32 | 31.4% faster| | string | 1024 | 32 | 31.89 | 21.49 | 32.6% faster| | large_string | 1024 | 32 | 31.75 | 22.01 | 30.7% faster| | string_view | 1024 | 128 | 49.51 | 36.11 | 27.1% faster| | string | 1024 | 128 | 48.91 | 34.90 | 28.6% faster| | large_string | 1024 | 128 | 49.78 | 35.42 | 28.8% faster| | string_view | 4096 | 32 | 133.67 | 95.93 | 28.2% faster| | string | 4096 | 32 | 131.48 | 91.73 | 30.2% faster| | large_string | 4096 | 32 | 129.61 | 92.82 | 28.4% faster| | string_view | 4096 | 128 | 191.50 | 153.74 | 19.7% faster| | string | 4096 | 128 | 185.27 | 149.37 | 19.4% faster| | large_string | 4096 | 128 | 187.82 | 154.32 | 17.8% faster|
1 parent bb4e0ec commit c10d410

File tree

3 files changed

+224
-15
lines changed

3 files changed

+224
-15
lines changed

datafusion/functions/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ harness = false
210210
name = "repeat"
211211
required-features = ["string_expressions"]
212212

213+
[[bench]]
214+
harness = false
215+
name = "replace"
216+
required-features = ["string_expressions"]
217+
213218
[[bench]]
214219
harness = false
215220
name = "random"
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
extern crate criterion;
19+
20+
use arrow::array::OffsetSizeTrait;
21+
use arrow::datatypes::{DataType, Field};
22+
use arrow::util::bench_util::{
23+
create_string_array_with_len, create_string_view_array_with_len,
24+
};
25+
use criterion::{Criterion, SamplingMode, criterion_group, criterion_main};
26+
use datafusion_common::DataFusionError;
27+
use datafusion_common::config::ConfigOptions;
28+
use datafusion_expr::{ColumnarValue, ScalarFunctionArgs};
29+
use datafusion_functions::string;
30+
use std::hint::black_box;
31+
use std::sync::Arc;
32+
use std::time::Duration;
33+
34+
fn create_args<O: OffsetSizeTrait>(
35+
size: usize,
36+
str_len: usize,
37+
force_view_types: bool,
38+
) -> Vec<ColumnarValue> {
39+
if force_view_types {
40+
let string_array =
41+
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+
vec![
45+
ColumnarValue::Array(string_array),
46+
ColumnarValue::Array(from_array),
47+
ColumnarValue::Array(to_array),
48+
]
49+
} else {
50+
let string_array =
51+
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+
55+
vec![
56+
ColumnarValue::Array(string_array),
57+
ColumnarValue::Array(from_array),
58+
ColumnarValue::Array(to_array),
59+
]
60+
}
61+
}
62+
63+
fn invoke_replace_with_args(
64+
args: Vec<ColumnarValue>,
65+
number_rows: usize,
66+
) -> Result<ColumnarValue, DataFusionError> {
67+
let arg_fields = args
68+
.iter()
69+
.enumerate()
70+
.map(|(idx, arg)| Field::new(format!("arg_{idx}"), arg.data_type(), true).into())
71+
.collect::<Vec<_>>();
72+
let config_options = Arc::new(ConfigOptions::default());
73+
74+
string::replace().invoke_with_args(ScalarFunctionArgs {
75+
args,
76+
arg_fields,
77+
number_rows,
78+
return_field: Field::new("f", DataType::Utf8, true).into(),
79+
config_options: Arc::clone(&config_options),
80+
})
81+
}
82+
83+
fn criterion_benchmark(c: &mut Criterion) {
84+
for size in [1024, 4096] {
85+
let mut group = c.benchmark_group(format!("replace size={size}"));
86+
group.sampling_mode(SamplingMode::Flat);
87+
group.sample_size(10);
88+
group.measurement_time(Duration::from_secs(10));
89+
90+
// Small strings
91+
let str_len = 32;
92+
let args = create_args::<i32>(size, str_len, true);
93+
group.bench_function(
94+
format!("replace_string_view [size={size}, str_len={str_len}]"),
95+
|b| {
96+
b.iter(|| {
97+
let args_cloned = args.clone();
98+
black_box(invoke_replace_with_args(args_cloned, size))
99+
})
100+
},
101+
);
102+
103+
let args = create_args::<i32>(size, str_len, false);
104+
group.bench_function(
105+
format!("replace_string [size={size}, str_len={str_len}]"),
106+
|b| {
107+
b.iter(|| {
108+
let args_cloned = args.clone();
109+
black_box(invoke_replace_with_args(args_cloned, size))
110+
})
111+
},
112+
);
113+
114+
let args = create_args::<i64>(size, str_len, false);
115+
group.bench_function(
116+
format!("replace_large_string [size={size}, str_len={str_len}]"),
117+
|b| {
118+
b.iter(|| {
119+
let args_cloned = args.clone();
120+
black_box(invoke_replace_with_args(args_cloned, size))
121+
})
122+
},
123+
);
124+
125+
// Larger strings
126+
let str_len = 128;
127+
let args = create_args::<i32>(size, str_len, true);
128+
group.bench_function(
129+
format!("replace_string_view [size={size}, str_len={str_len}]"),
130+
|b| {
131+
b.iter(|| {
132+
let args_cloned = args.clone();
133+
black_box(invoke_replace_with_args(args_cloned, size))
134+
})
135+
},
136+
);
137+
138+
let args = create_args::<i32>(size, str_len, false);
139+
group.bench_function(
140+
format!("replace_string [size={size}, str_len={str_len}]"),
141+
|b| {
142+
b.iter(|| {
143+
let args_cloned = args.clone();
144+
black_box(invoke_replace_with_args(args_cloned, size))
145+
})
146+
},
147+
);
148+
149+
let args = create_args::<i64>(size, str_len, false);
150+
group.bench_function(
151+
format!("replace_large_string [size={size}, str_len={str_len}]"),
152+
|b| {
153+
b.iter(|| {
154+
let args_cloned = args.clone();
155+
black_box(invoke_replace_with_args(args_cloned, size))
156+
})
157+
},
158+
);
159+
160+
group.finish();
161+
}
162+
}
163+
164+
criterion_group!(benches, criterion_benchmark);
165+
criterion_main!(benches);

datafusion/functions/src/string/replace.rs

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use std::any::Any;
1919
use std::sync::Arc;
2020

21-
use arrow::array::{ArrayRef, GenericStringArray, OffsetSizeTrait, StringArray};
21+
use arrow::array::{ArrayRef, GenericStringBuilder, OffsetSizeTrait};
2222
use arrow::datatypes::DataType;
2323

2424
use crate::utils::{make_scalar_function, utf8_to_str_type};
@@ -165,17 +165,25 @@ fn replace_view(args: &[ArrayRef]) -> Result<ArrayRef> {
165165
let from_array = as_string_view_array(&args[1])?;
166166
let to_array = as_string_view_array(&args[2])?;
167167

168-
let result = string_array
168+
let mut builder = GenericStringBuilder::<i32>::new();
169+
let mut buffer = String::new();
170+
171+
for ((string, from), to) in string_array
169172
.iter()
170173
.zip(from_array.iter())
171174
.zip(to_array.iter())
172-
.map(|((string, from), to)| match (string, from, to) {
173-
(Some(string), Some(from), Some(to)) => Some(string.replace(from, to)),
174-
_ => None,
175-
})
176-
.collect::<StringArray>();
175+
{
176+
match (string, from, to) {
177+
(Some(string), Some(from), Some(to)) => {
178+
buffer.clear();
179+
replace_into_string(&mut buffer, string, from, to);
180+
builder.append_value(&buffer);
181+
}
182+
_ => builder.append_null(),
183+
}
184+
}
177185

178-
Ok(Arc::new(result) as ArrayRef)
186+
Ok(Arc::new(builder.finish()) as ArrayRef)
179187
}
180188

181189
/// Replaces all occurrences in string of substring from with substring to.
@@ -185,17 +193,48 @@ fn replace<T: OffsetSizeTrait>(args: &[ArrayRef]) -> Result<ArrayRef> {
185193
let from_array = as_generic_string_array::<T>(&args[1])?;
186194
let to_array = as_generic_string_array::<T>(&args[2])?;
187195

188-
let result = string_array
196+
let mut builder = GenericStringBuilder::<T>::new();
197+
let mut buffer = String::new();
198+
199+
for ((string, from), to) in string_array
189200
.iter()
190201
.zip(from_array.iter())
191202
.zip(to_array.iter())
192-
.map(|((string, from), to)| match (string, from, to) {
193-
(Some(string), Some(from), Some(to)) => Some(string.replace(from, to)),
194-
_ => None,
195-
})
196-
.collect::<GenericStringArray<T>>();
203+
{
204+
match (string, from, to) {
205+
(Some(string), Some(from), Some(to)) => {
206+
buffer.clear();
207+
replace_into_string(&mut buffer, string, from, to);
208+
builder.append_value(&buffer);
209+
}
210+
_ => builder.append_null(),
211+
}
212+
}
213+
214+
Ok(Arc::new(builder.finish()) as ArrayRef)
215+
}
197216

198-
Ok(Arc::new(result) as ArrayRef)
217+
/// Helper function to perform string replacement into a reusable String buffer
218+
#[inline]
219+
fn replace_into_string(buffer: &mut String, string: &str, from: &str, to: &str) {
220+
if from.is_empty() {
221+
// When from is empty, insert 'to' at the beginning, between each character, and at the end
222+
// This matches the behavior of str::replace()
223+
buffer.push_str(to);
224+
for ch in string.chars() {
225+
buffer.push(ch);
226+
buffer.push_str(to);
227+
}
228+
return;
229+
}
230+
231+
let mut last_end = 0;
232+
for (start, _part) in string.match_indices(from) {
233+
buffer.push_str(&string[last_end..start]);
234+
buffer.push_str(to);
235+
last_end = start + from.len();
236+
}
237+
buffer.push_str(&string[last_end..]);
199238
}
200239

201240
#[cfg(test)]

0 commit comments

Comments
 (0)