Skip to content

perf: close remaining 1.8x regex performance gap vs native Perl #1907

Closed

Description

@fglock

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 repeated Long.toString() calls for variables that alternate between integer and string contexts.
  • Reduce PerlRuntime.current() calls: The state() helper calls PerlRuntime.current() (ThreadLocal read) multiple times per match. Could pass the PerlRuntime instance through the call chain.

Moderate impact, higher complexity

  • Joni Matcher reuse/pooling: The Matcher hierarchy (ByteCodeMachine → StackMachine → Matcher) is created fresh per match because bytes/str/end are final. A reset() method in the vendored Joni fork could allow reuse for different inputs.
  • Bytecode-emitted warning state: Move emitRegexWarningState work 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_ENCODINGS cache 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.pl

Related

Activity

  1. added a commit that references this issue on Oct 27, 2026
    3b00c2c
  2. fglock commented on Oct 27, 2026

    @fglock
    OwnerAuthor

    Round 5 optimizations pushed to PR #1908:

    • Literal fast path: patterns like /42/ bypass the Joni engine entirely using String.indexOf()
    • Merged ThreadLocal: combined two per-match ThreadLocal reads into one shared holder
    • Integer string cache: cached toString() for integers 0-511

    Result: ~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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions