Repository navigation
perf: close remaining 1.8x regex performance gap vs native Perl #1907
Copy link
Copy link
Closed
Description
Context
PR #1905 reduced regex per-match overhead by ~2x (from ~10,453/s to ~22,100/s on the $i =~ /42/ benchmark). PerlOnJava is now ~1.8x slower than native Perl (~39,683/s) for simple regex matching.
This issue tracks closing the remaining gap to achieve near-parity with native Perl.
Profiling data (post-PR #1905)
Top remaining hotspots from async-profiler:
| % CPU | Source | Notes |
|---|---|---|
| 4.52% | RuntimeScalar.<init> |
Integer allocation overhead (loop iteration, not regex-specific) |
| 3.13% | HashMap.computeIfAbsent |
Encoding cache or other HashMap usage |
| 2.78% | JoniRegexMatcher.<init> |
New wrapper object per match |
| 2.44% | ThreadLocal.getEntry |
Remaining ThreadLocal reads (~3 per match) |
| 1.86% | StackMachine.<init> |
Joni internal — new matcher stack per match |
| 1.74% | getQuotedRegex |
Regex callsite cache lookup |
| 1.51% | RuntimeScalar.toStringLarge |
Integer→string conversion per match |
| 1.28% | JoniRegexPattern.byteInputEncoding |
Input encoding for byte-mode patterns |
Candidate optimizations
High impact, moderate complexity
- Integer-to-string dual-var caching: Cache the string representation in
RuntimeScalar(Perl's PV/IV dual-variable semantics). Would eliminate repeatedLong.toString()calls for variables that alternate between integer and string contexts. - Reduce
PerlRuntime.current()calls: Thestate()helper callsPerlRuntime.current()(ThreadLocal read) multiple times per match. Could pass thePerlRuntimeinstance through the call chain.
Moderate impact, higher complexity
- Joni Matcher reuse/pooling: The
Matcherhierarchy (ByteCodeMachine → StackMachine → Matcher) is created fresh per match becausebytes/str/endarefinal. Areset()method in the vendored Joni fork could allow reuse for different inputs. - Bytecode-emitted warning state: Move
emitRegexWarningStatework from runtime to the bytecode emitter, avoiding per-match ThreadLocal reads for warning state.
Lower impact
- Encoding cache bypass for varying inputs: The synchronized
INPUT_ENCODINGScache doesn't help when the input string changes every match (as in the benchmark). Consider a thread-local LRU or skip caching entirely for short ASCII strings. - JoniRegexMatcher simplification: For simple patterns (no /g, no callbacks, no locale), a lighter-weight matcher path could skip many conditional checks.
Benchmark
timeout 60 ./jperl dev/bench/benchmark_regex.pl
# Native Perl: perl dev/bench/benchmark_regex.plRelated
- Regex match performance: ~4x slower than native Perl #1904 — original issue (closed by perf: regex hot-path optimizations — ~2x throughput improvement #1905)
- perf: regex hot-path optimizations — ~2x throughput improvement #1905 — rounds 1–3 optimizations
Metadata
Metadata
Assignees
Labels
No labels
Activity
perf: round 4 regex hot-path — reduce ThreadLocal reads, fast INTEGER…
fglock commentedon Oct 27, 2026
Round 5 optimizations pushed to PR #1908:
/42/bypass the Joni engine entirely usingString.indexOf()toString()for integers 0-511Result: ~47,000 iter/s vs native Perl ~50,000 iter/s (1.07x gap, down from 1.8x at issue open)
The remaining ~7% gap is likely inherent JVM overhead (method dispatch, safepoint checks, bounds checking) that can't be eliminated without deeper architectural changes like moving the warning state emit into the regex compilation rather than per-match bytecode.