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