You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by ur...@apache.org on 2026/09/22 10:15:38 UTC

(spark) branch branch-4.x updated: [SPARK-59531][SQL] Avoid a per-match MatchResult allocation in regexp_extract, regexp_extract_all, and regexp_instr

This is an automated email from the ASF dual-hosted git repository.

uros-b pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new cb54bc6ca3e8 [SPARK-59531][SQL] Avoid a per-match MatchResult allocation in regexp_extract, regexp_extract_all, and regexp_instr
cb54bc6ca3e8 is described below

commit cb54bc6ca3e864d9aa3008fd34361e4af198c2cb
Author: david-mollitor-db <da...@databricks.com>
AuthorDate: Tue Sep 22 10:15:18 2026 +0000

    [SPARK-59531][SQL] Avoid a per-match MatchResult allocation in regexp_extract, regexp_extract_all, and regexp_instr
    
    ### What changes were proposed in this pull request?
    
    `RegExpExtractBase.extract`, `RegExpExtractBase.extractAll`, and `RegExpInStr` call
    `Matcher.toMatchResult()` on every match, then read `groupCount`/`group(idx)` (the extract
    helpers) or `start()` (`regexp_instr`) from the returned snapshot:
    
    ```scala
    if (matcher.find()) {
      val mr = matcher.toMatchResult
      checkGroupIndex(prettyName, mr.groupCount, idx)
      val group = mr.group(idx)
      ...
    }
    ```
    
    `Matcher.toMatchResult()` allocates an immutable `MatchResult` snapshot of the current match
    state, including a copy of the groups `int[]`. This PR reads the same values directly from the
    `Matcher`:
    
    ```scala
    if (matcher.find()) {
      checkGroupIndex(prettyName, matcher.groupCount, idx)
      val group = matcher.group(idx)
      ...
    }
    ```
    
    and `m.toMatchResult().start()` -> `m.start()` in `RegExpInStr` (both the interpreted and the
    codegen paths).
    
    ### Why are the changes needed?
    
    The values are read immediately after `find()`, with nothing else mutating the matcher in
    between, so `Matcher.groupCount()`, `Matcher.group(int)`, and `Matcher.start()` return exactly
    the same values as the corresponding `MatchResult` methods for the current match. Snapshotting
    into a `MatchResult` first only adds one allocation (the snapshot plus a copy of the groups
    `int[]`) per match on the `regexp_extract`, `regexp_extract_all`, and `regexp_instr` hot paths.
    
    Profiling a `regexp_extract` workload under JFR (`settings=profile`) shows
    `java.util.regex.Matcher.toMatchResult` accounting for ~5% of sampled allocation pressure;
    this change removes it. Wall-clock time is unchanged within run-to-run noise (this is an
    allocation / GC-pressure reduction, not a CPU one).
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Existing tests, which cover `regexp_extract`, `regexp_extract_all`, and `regexp_instr`:
    
    ```
    build/sbt 'catalyst/testOnly *RegexpExpressionsSuite'
    ```
    
    All 24 tests pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Isaac
    
    This pull request and its description were written by Isaac.
    
    Closes #58815 from david-mollitor-db/SPARK-59531.
    
    Authored-by: david-mollitor-db <da...@databricks.com>
    Signed-off-by: Uros Bojanic <22...@users.noreply.github.com>
    (cherry picked from commit 01564f24e48415d562cc0023cc2d107f023de792)
    Signed-off-by: Uros Bojanic <22...@users.noreply.github.com>
---
 .../spark/sql/catalyst/expressions/regexpExpressions.scala | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/regexpExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/regexpExpressions.scala
index 5c1fb73cf1a8..b16ff1a6ed41 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/regexpExpressions.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/regexpExpressions.scala
@@ -829,9 +829,8 @@ object RegExpExtractBase {
   // generated Java is a single call rather than an inline match/group block.
   def extract(matcher: Matcher, idx: Int, prettyName: String): UTF8String = {
     if (matcher.find()) {
-      val mr = matcher.toMatchResult
-      checkGroupIndex(prettyName, mr.groupCount, idx)
-      val group = mr.group(idx)
+      checkGroupIndex(prettyName, matcher.groupCount, idx)
+      val group = matcher.group(idx)
       // Pattern matched, but it's an optional group
       if (group == null) UTF8String.EMPTY_UTF8 else UTF8String.fromString(group)
     } else {
@@ -843,9 +842,8 @@ object RegExpExtractBase {
   def extractAll(matcher: Matcher, idx: Int, prettyName: String): GenericArrayData = {
     val matchResults = new ArrayBuffer[UTF8String]()
     while (matcher.find()) {
-      val mr = matcher.toMatchResult
-      checkGroupIndex(prettyName, mr.groupCount, idx)
-      val group = mr.group(idx)
+      checkGroupIndex(prettyName, matcher.groupCount, idx)
+      val group = matcher.group(idx)
       // Pattern matched, but it's an optional group
       if (group == null) {
         matchResults += UTF8String.EMPTY_UTF8
@@ -1173,7 +1171,7 @@ case class RegExpInStr(subject: Expression, regexp: Expression, idx: Expression)
       val source = s.toString
       val m = getLastMatcher(source, r)
       if (m.find) {
-        source.codePointCount(0, m.toMatchResult.start()) + 1
+        source.codePointCount(0, m.start()) + 1
       } else {
         0
       }
@@ -1202,7 +1200,7 @@ case class RegExpInStr(subject: Expression, regexp: Expression, idx: Expression)
         collationId)}
          |  if ($matcher.find()) {
          |    String $source = $subject.toString();
-         |    ${ev.value} = $source.codePointCount(0, $matcher.toMatchResult().start()) + 1;
+         |    ${ev.value} = $source.codePointCount(0, $matcher.start()) + 1;
          |  } else {
          |    ${ev.value} = 0;
          |  }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org