perf: continue eval STRING optimization work (follow-up to #1940)

Ouverte
#1,961 1 commentaire 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

@fglock y travaille déjà.

Depuis le 6/11/2026.

  • #1984 par @fglock — ouverte
  • #1985 par @fglock — ouverte

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
52/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
Active
Stack technique
java, perl
Domaine
backend, performance

Piste de recherche

Start with src/main/java/org/perlonjava/frontend/parser/Parser.java and the listed allocation hotspots; run dev/bench/benchmark_eval_string.pl to establish a baseline. The issue lists five possible optimizations across parser, emitter, lexer, runtime, and symbol table, so pick one and measure its impact. Run nice -n 19 make and verify both backends with the provided eval commands; done means the change passes and improves the benchmark without breaking those checks.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

Context

PR #1940 reduced eval STRING overhead from 6.8× to 6.0× slower than system Perl on dev/bench/benchmark_eval_string.pl:

ops/sec
System Perl 263,000/s
PerlOnJava (post-PR #1940) 43,700/s
Ratio 6.0×

All optimizations in PR #1940 targeted the real pipeline per unique eval string (no eval-string caching). The remaining gap is structural — JVM overhead vs native C.

Remaining opportunities (in estimated impact order)

1. Lazy indexSourceLines() in Parser — high impact

File: src/main/java/org/perlonjava/frontend/parser/Parser.java (~lines 383–394)

indexSourceLines() is called from both constructors and scans the entire token list to build a line-number index array. For eval strings, no source-line lookup is needed at runtime unless an exception is thrown.

Fix: Remove the call from both constructors. Build the int[] lazily on the first sourceLineAt() call (add a boolean lineIndexBuilt field, or check lineIndex == null).

Evidence: This was the #1 allocation hotspot in a JFR profile of 500k iterations (416 samples). Saving one int[] allocation + O(tokens) scan per eval.


2. Lazy EmitterContext.contextCache and JavaClassInfo maps — medium impact

Files:

  • src/main/java/org/perlonjava/backend/jvm/EmitterContext.java
  • src/main/java/org/perlonjava/backend/jvm/JavaClassInfo.java

EmitterContext.contextCache is a HashMap allocated eagerly in the constructor even for evals that never write to it.

JavaClassInfo has 4 pin/literal maps allocated eagerly.

Fix: Initialize each to null; create on first write via a private accessor. Readers check for null (return empty or no-op).

Savings: 5 map allocations per eval.


3. Lexer sawBegin flag to skip BEGIN aliasing loop — medium impact

Files:

  • src/main/java/org/perlonjava/frontend/lexer/Lexer.java
  • src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java

evalStringWithInterpreter() (RuntimeCode.java around line 5330) iterates ALL visible variables on every eval to support BEGIN {} inside eval strings. BEGIN in eval is extremely rare.

Fix: Add boolean sawBegin to Lexer. In nextToken(), when creating an IDENTIFIER token, check if text.equals("BEGIN") and set sawBegin = true. After tokenize(), evalStringWithInterpreter() checks lexer.sawBegin — if false, skip the entire alias loop.

Savings: O(visible_vars) String allocations + HashMap lookups eliminated on the common path.


4. Skip BitSet.clone() for empty BitSets in enterScope — low impact

File: src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java

enterScope() unconditionally clones 3 BitSets even when they are empty (the common case for eval strings with no warning pragmas).

Fix: Replace existing.clone() with new BitSet() when existing.isEmpty().


5. Pool common LexerToken instances — low impact

File: src/main/java/org/perlonjava/frontend/lexer/Lexer.java

NEWLINE and common single-char operator tokens (,, ;, (, ), {, }) are allocated fresh on every tokenization. These are stateless value objects.

Fix: Static flyweight instances; return pooled instance from nextToken() for these common cases.


Measurement guidance

Benchmark command:

# Warm up JVM + measure
timeout 120 ./jperl dev/bench/benchmark_eval_string.pl

# System Perl baseline
perl dev/bench/benchmark_eval_string.pl

# Quick smoke (no JVM warmup noise):
timeout 30 ./jperl -e 'use Benchmark qw(timethis); timethis(500_000, sub { eval "1" })'

For each change, run nice -n 19 make first (full test suite). Both backends must still pass:

./jperl -e 'eval "my \$x = 1"'
./jperl --interpreter -e 'eval "my \$x = 1"'

Profile guidance

The JFR profile that informed PR #1940 used:

./jperl -XX:+UnlockDiagnosticVMOptions -XX:StartFlightRecording=... -e '
  for (1..500_000) { eval "my \$x = 1 + 2; \$x" }
'

A fresh profile after PR #1940 merges will show whether the allocation hotspot has shifted.

Notes

  • The a59926c1a/442ce8c09 eval-string cache experiment was tried and removed — it caused correctness issues and the cache key was too coarse. Do not re-add a simple string-equality cache for eval.
  • The PerlRuntime.bind() re-entrant fast-path skips synchronized lifecycleMonitor but still pushes a BindingFrame; the LIFO invariant in PerlRuntimeTest must stay green.
Langage dominant
Perl
Étoiles
65
Forks
8
Merge moyen
5 h 54 min
PR mergées (30 j)
184

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de fglock/PerlOnJava

Toutes les issues de fglock/PerlOnJava

Issues similaires

Plus d'issues Perl

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.