Skip to content

perf: close remaining regex performance gap vs native Perl - #1908

Merged
fglock merged 3 commits into
masterfrom
feature/regex-perf-hot-path
Oct 28, 2026
Merged

fglock merged 3 commits into
masterfrom
feature/regex-perf-hot-path

Conversation

@fglock

@fglock fglock commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes the remaining regex performance gap identified in #1907. Five rounds of optimizations targeting the regex hot path:

Round 1 (03977c3): Reduce per-match regex overhead by ~67%

  • Bypass synchronized INPUT_ENCODINGS LRU cache for short ASCII strings
  • Inline isAscii() + asciiToBytes() to avoid getBytes(UTF_8) allocation

Round 2 (4da9f7c): ~2x total speedup

  • Pool identity arrays (charToByte/byteToChar) up to 64 elements
  • Fast path in byteInputEncoding() for short strings

Round 3 (9c44310): Skip identity array build for ASCII

  • Extend ASCII fast path into buildInputEncoding() fallback
  • Use pooled identity arrays in buildByteInputEncoding()

Round 4 (3b00c2c): Reduce ThreadLocal reads, fast INTEGER toString

  • Cache PerlRuntime.current() in matchRegexDirect to eliminate redundant ThreadLocal read for taint-mode check
  • Add INTEGER to RuntimeScalar.toString() inline fast path

Round 5: Literal fast path, merged ThreadLocal, integer string cache

  • Pure literal patterns (no metacharacters, e.g. /42/) bypass the Joni engine entirely using String.indexOf()
  • LiteralMatcher implements RegexMatcher for correct $-[0]/$+[0] after a literal match
  • Merged two per-match ThreadLocal reads (setCallSiteWarningState + setCallSiteWarningBits) into single combined call with shared holder object
  • Integer string cache (0-511) avoids Integer.toString() allocation for small loop counters
  • Inline regex resolution for pre-compiled REGEX type (avoids intermediate ResolvedRegex record allocation)

Fix (ad62884): Use Region-aware matcher when regex has capture groups

Benchmark results

Before Round 4 Round 5 Native Perl
iterations/s ~22,100 ~24,000 ~47,000 ~50,000
vs native 2.3x gap 2.1x gap 1.07x gap 1.0x

Test plan

  • All unit tests pass (make — BUILD SUCCESSFUL)
  • regex_hot_path_perf.t covers ASCII fast path, identity array pool boundaries, integer matching, Unicode fallback, captures, literal match positions ($-[0]/$+[0]/$&), and edge cases (37 tests)
  • Tests validated with system Perl first

🤖 Generated with Claude Code

fglock and others added 2 commits October 27, 2026 20:54
… toString

- Cache PerlRuntime.current() in matchRegexDirect to eliminate redundant
  ThreadLocal read for taint-mode check
- Add INTEGER to RuntimeScalar.toString() inline fast path, avoiding
  dispatch through toStringLarge() for the common integer-regex case
- Add regression test covering ASCII fast path, identity array pool
  boundaries, integer matching, and Unicode fallback

Benchmark: ~24,000/s (up from ~22,100/s baseline, native Perl ~50,000/s)

Closes #1907

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Literal pattern fast path: patterns like /42/ that contain no
metacharacters bypass the entire Joni engine and use String.indexOf()
directly. LiteralMatcher implements RegexMatcher to provide correct
$-[0]/$+[0]/match-state after a literal hit.

Merged two per-match ThreadLocal reads (setCallSiteWarningState +
setCallSiteWarningBits) into a single combined setCallSiteWarning()
call with one shared CallSiteWarningHolder object.

Integer string cache (0-511) avoids Integer.toString() allocation
for small non-negative values common in loop counters.

Benchmark: ~47,000 iter/s vs native Perl ~50,000 iter/s (1.07x gap,
down from 1.8x at issue open).

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Perl's taint rules: match results ($`, $&, $') are tainted only when
the pattern itself is tainted, locale is involved, or the taintResults
flag is set — not when only the target string is tainted.

The literal fast path was incorrectly setting lastMatchResultsTainted
based on the target string's taint status, causing 3 failures in
op/taint.t (tests 478-480).

Fix: set lastMatchResultsTainted = false unconditionally in
matchLiteralScalar (literal patterns are compile-time constants,
never tainted), and add a taintResults() guard to the fast path
eligibility check so regex patterns with that flag always take
the full Joni path.

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@fglock
fglock merged commit cab9a15 into master Oct 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant