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);

Reply via email to