This is an automated email from the ASF dual-hosted git repository.
alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new ebb524f6eb perf: Reuse capture locations in `regexp_match` (#11274)
ebb524f6eb is described below
commit ebb524f6ebe4df453ab9f7945d80320e5bcf3718
Author: Neil Conway <[email protected]>
AuthorDate: Wed Sep 30 12:28:39 2026 -0400
perf: Reuse capture locations in `regexp_match` (#11274)
# Which issue does this PR close?
- N/A
# Rationale for this change
When `regexp_match` is used with a scalar pattern, we previously
allocated a new `CaptureLocations` value for every row that was matched
against. We can do better by allocating the storage once and then
reusing it for every row via the `Regex::captures_read()` API.
Benchmarks: (M4 Max)
- Utf8, matching: 1,463.97 → 1,070.79 µs — 26.9% less time.
- Utf8, mixed: 829.28 → 607.46 µs — 26.7% less time.
- Utf8, no matches: 299.01 → 148.47 µs — 50.3% less time.
- Utf8View, matching: 1,496.74 → 1,159.14 µs — 22.6% less time.
- Utf8View, mixed: 849.78 → 649.64 µs — 23.6% less time.
- Utf8View, no matches: 253.79 → 151.30 µs — 40.4% less time.
# What changes are included in this PR?
* Switch to `Regex::captures_read` in `regexp_scalar_match` and
`regexp_scalar_match_utf8view`
* Add benchmark
* Improve test coverage of scalar regexp matching
# Are these changes tested?
Yes. New tests added to increase test coverage of this function overall.
# Are there any user-facing changes?
No.
---
arrow-string/src/regexp.rs | 71 ++++++++++++++++++++++++++++++++++-------
arrow/benches/regexp_kernels.rs | 45 +++++++++++++++++++++++---
2 files changed, 101 insertions(+), 15 deletions(-)
diff --git a/arrow-string/src/regexp.rs b/arrow-string/src/regexp.rs
index 0474f2a7b8..015b8b4778 100644
--- a/arrow-string/src/regexp.rs
+++ b/arrow-string/src/regexp.rs
@@ -332,6 +332,8 @@ fn get_scalar_pattern_flag_utf8view<'a>(
macro_rules! process_regexp_match {
($array:expr, $regex:expr, $list_builder:expr) => {
+ // Reuse capture location storage across matches
+ let mut capture_locations = None;
$array
.iter()
.map(|value| {
@@ -342,19 +344,25 @@ macro_rules! process_regexp_match {
$list_builder.values().append_value("");
$list_builder.append(true);
}
- Some(value) => match $regex.captures(value) {
- Some(caps) => {
- let mut iter = caps.iter();
- if caps.len() > 1 {
- iter.next();
- }
- for m in iter.flatten() {
-
$list_builder.values().append_value(m.as_str());
+ Some(value) => {
+ let locations =
+ capture_locations.get_or_insert_with(||
$regex.capture_locations());
+ match $regex.captures_read(locations, value) {
+ Some(_) => {
+ let mut groups = 0..locations.len();
+ if locations.len() > 1 {
+ // Skip group 0 (the whole match) when
explicit capture groups exist.
+ groups.next();
+ }
+ let iter = groups.map(|group|
locations.get(group));
+ for (start, end) in iter.flatten() {
+
$list_builder.values().append_value(&value[start..end]);
+ }
+ $list_builder.append(true);
}
- $list_builder.append(true);
+ None => $list_builder.append(false),
}
- None => $list_builder.append(false),
- },
+ }
None => $list_builder.append(false),
}
Ok(())
@@ -519,6 +527,47 @@ pub fn regexp_match(
mod tests {
use super::*;
+ #[test]
+ fn match_scalar_pattern_capture_groups() {
+ macro_rules! check {
+ ($array_type:ty, $builder_type:ty) => {{
+ // "β" follows "aβ" to check that an optional group matched in
+ // one row is not reported for the next row.
+ let array = <$array_type>::from(vec![Some("aβ"), Some("β"),
Some("none"), None]);
+ let cases: [(&str, [Option<&[&str]>; 4]); 4] = [
+ // Optional groups that did not match are omitted.
+ ("(a)?(β)", [Some(&["a", "β"]), Some(&["β"]), None, None]),
+ // Without explicit groups, the whole match is returned.
+ ("β", [Some(&["β"]), Some(&["β"]), None, None]),
+ ("()", [Some(&[""]), Some(&[""]), Some(&[""]), None]),
+ ("", [Some(&[""]), Some(&[""]), Some(&[""]), None]),
+ ];
+ for (pattern, rows) in cases {
+ let mut expected =
ListBuilder::new(<$builder_type>::new());
+ for row in rows {
+ match row {
+ Some(values) => {
+ for value in values {
+ expected.values().append_value(value);
+ }
+ expected.append(true);
+ }
+ None => expected.append(false),
+ }
+ }
+ let expected = expected.finish();
+
+ let pattern_scalar =
Scalar::new(<$array_type>::from(vec![pattern]));
+ let actual = regexp_match(&array, &pattern_scalar,
None).unwrap();
+ assert_eq!(actual.as_list::<i32>(), &expected,
"{pattern}");
+ }
+ }};
+ }
+ check!(StringArray, GenericStringBuilder<i32>);
+ check!(LargeStringArray, GenericStringBuilder<i64>);
+ check!(StringViewArray, StringViewBuilder);
+ }
+
macro_rules! test_match_single_group {
($test_name:ident, $values:expr, $patterns:expr, $arr_type:ty,
$builder_type:ty, $expected:expr) => {
#[test]
diff --git a/arrow/benches/regexp_kernels.rs b/arrow/benches/regexp_kernels.rs
index f2483f29f4..40e25cb75b 100644
--- a/arrow/benches/regexp_kernels.rs
+++ b/arrow/benches/regexp_kernels.rs
@@ -17,15 +17,52 @@
#[macro_use]
extern crate criterion;
-use criterion::Criterion;
+use criterion::{BenchmarkId, Criterion, Throughput};
use arrow::array::*;
use arrow::compute::kernels::regexp::*;
use arrow::util::bench_util::*;
use std::hint;
-fn bench_regexp(arr: &GenericStringArray<i32>, regex_array: &dyn Datum) {
- regexp_match(hint::black_box(arr), regex_array, None).unwrap();
+fn bench_regexp(arr: &dyn Array, regex_array: &dyn Datum) {
+ hint::black_box(
+ regexp_match(hint::black_box(arr), hint::black_box(regex_array),
None).unwrap(),
+ );
+}
+
+fn bench_scalar_captures(c: &mut Criterion) {
+ let size = 8192;
+ let matches = StringArray::from_iter_values(
+ (0..size).map(|i| format!("customer-{i:08}-region-{}-purchase", i %
8)),
+ );
+ let no_matches = StringArray::from_iter_values(
+ (0..size).map(|i| format!("visitor-{i:08}-region-{}-purchase", i % 8)),
+ );
+ let mixed = StringArray::from_iter((0..size).map(|i| match i % 4 {
+ 0 => None,
+ 2 => Some(no_matches.value(i)),
+ _ => Some(matches.value(i)),
+ }));
+ let pattern = r"customer-([0-9]+)-region-([0-7])";
+ let utf8_pattern = Scalar::new(StringArray::from(vec![pattern]));
+ let view_pattern = Scalar::new(StringViewArray::from(vec![pattern]));
+
+ let mut group = c.benchmark_group("regexp_scalar_captures");
+ group.throughput(Throughput::Elements(size as u64));
+ for (name, array) in [
+ ("matches", matches),
+ ("mixed", mixed),
+ ("no_matches", no_matches),
+ ] {
+ let view = StringViewArray::from(&array);
+ group.bench_function(BenchmarkId::new("utf8", name), |b| {
+ b.iter(|| bench_regexp(&array, &utf8_pattern))
+ });
+ group.bench_function(BenchmarkId::new("utf8view", name), |b| {
+ b.iter(|| bench_regexp(&view, &view_pattern))
+ });
+ }
+ group.finish();
}
fn add_benchmark(c: &mut Criterion) {
@@ -46,5 +83,5 @@ fn add_benchmark(c: &mut Criterion) {
});
}
-criterion_group!(benches, add_benchmark);
+criterion_group!(benches, add_benchmark, bench_scalar_captures);
criterion_main!(benches);