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

オープン
#1,961 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
52/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
java, perl

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

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.
主要言語
Perl
スター
65
フォーク
8
平均マージ
5時間 45分
マージ済み PR(30日)
184

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

fglock/PerlOnJava のほかの issue

fglock/PerlOnJava の issue をすべて見る

似ている issue

Perl の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。