You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Reduce the performance gap for repeated string eval #1911
Investigate the repeated string-eval path and reduce the performance difference while preserving Perl behavior. Keep the benchmark reproducible and compare both runtimes under matched conditions.
Java version, OS, Perl version, and PerlOnJava commit were not recorded with this run.
Branch:feature/regex-perf-hot-path Status: Build running, changes implemented but not yet verified
What was done
JFR profiling of 1M eval '1' iterations identified the per-eval ceremony (not parsing) as the bottleneck — 15 µs/eval on PerlOnJava vs <1 µs on system Perl. The overhead is dominated by object allocation, map construction, and repeated computation of unchanged values.
Six optimizations implemented targeting the top JFR hotspots:
Optimization
File
JFR evidence
Replace IdentityHashMap with int[] for source-line offsets
#1 CPU hotspot (9%), avoids 2× ConcurrentHashMap iteration
Skip BitSet.clone() for empty BitSets in enterScope()
ScopedSymbolTable.java
118 alloc samples
Lazy hint hash %^H save/restore when empty
RuntimeCode.java, EvalStringHandler.java
Avoids HashMap + N RuntimeScalar copies
Lazy EmitterContext.contextCache
EmitterContext.java
97 alloc samples
Lazy JavaClassInfo pin/literal maps
JavaClassInfo.java
135 alloc samples
Not implemented yet (deferred due to complexity/risk ratio):
Lazy goto collections in BytecodeCompiler (~12 collections, 20+ usage sites). Would eliminate 254 ArrayDeque + 142 HashSet allocations per eval. Safe but needs careful null-checking at all read/write sites.
InterpretedCode.shouldKeepRuntimeDeparseSource() called File.isFile() on every eval — 16% of CPU doing a filesystem stat on (eval N) names that are never real files
WarningFlags.toWarningBitsString() iterated ConcurrentHashMap (~40 entries) twice per eval (~12.5% of CPU) — add last-result cache
Return Collections.emptySet() when disabled-warnings BitSet is empty (the common case)
Benchmarks
Micro-benchmark (eval '1'):
Before
After
Change
jperl
~15 µs/eval
8.3 µs/eval
−45%
system Perl
0.9 µs/eval
—
baseline
gap
~17x
~9x
narrowed
Full benchmark (benchmark_eval_string.pl):
Before (issue)
After
system Perl
wall clock
47–63 s
25 s
3 s
throughput
~16–21k/s
41k/s
316k/s
Profiling method
Used async-profiler 4.5 (-e cpu) for CPU sampling and JFR for allocation profiling. The stat64 hotspot was invisible in JFR — async-profiler caught it because it profiles native frames.
Remaining hotspots (for future work)
ScopedSymbolTable.snapShot() + constructor: ~14 synchronized Stack.push/pop per eval
WarningBitsRegistry.register(): string concat + ConcurrentHashMap.put per eval
WarningFlags.toWarningBitsString cache depends on external mutable state — the per-ScopedSymbolTable cache was dropped because WarningFlags.getCustomWarningCategories() can change without any SST method being called
State: handed-off
Agent/session: Codex session 2026-10-29T15:53Z #1926-UAT, resumed 2026-10-29
Scope: #1911; restore op/attrs.t and op/const-optree.t behavior in the current checkout after local UAT still showed the regressions.
Work: current branch feature/regex-perf-hot-path, committed fix dc3be933e (fix: avoid context-sensitive eval cache hits (#1911)). Related open PR #1926 already contains the same source guard and focused regression coverage at head 9e560c09b87121756eab98e4fb4918f4de3c0522.
Completed: The current checkout no longer caches arbitrary eval source. It admits only short decimal integer literals, so evals with compile-time effects or runtime package name resolution are parsed again. Added focused coverage for repeated use attributes validation against changing reference types and dynamic ambiguous time dispatch. Added a changelog entry. The fix is committed locally, and the worktree is clean.
Evidence on current source dc3be933e: timeout 60 perl src/test/resources/unit/issue_1926_eval_cache_context.t passes 4/4; timeout 120 ./jperl ... and timeout 120 ./jperl --interpreter ... each pass 4/4; nice -n 19 timeout 1800 make passes; nice -n 19 timeout 600 make check-links passes; timeout 1200 perl dev/tools/perl_test_runner.pl --jperl ./jperl perl5_t/t/op/attrs.t perl5_t/t/op/const-optree.t passes 307/307 (159/159 and 148/148). The original failing counts were 147/159 and 147/148.
Blockers: none. The matching fix is already on open PR #1926; this checkout's validated commit is ready for the user's next branch/PR action. No merge or additional PR was performed.
Measured with dev/bench/benchmark_eval_string_stats.pl on master (f4f4962).
The "cached" scenario shows the interpreter eval cache helps, but per-eval ceremony (compilation lock, scope save/restore, hint hash, BEGIN aliases) still dominates. Next round targets those allocations.
ScopedSymbolTable.getWarningBitsString() was the #1 CPU hotspot in JFR profiles
(~22% of CPU time during eval loops). It iterates a ConcurrentHashMap twice on
every call. Result depends only on the current scope's warning BitSets, so it's
safe to cache and invalidate on mutation.
Added cachedWarningBitsString field, invalidated in all 12 external mutation sites
across 10 files (BytecodeCompiler, EmitCompilerFlag, EvalStringHandler, EmitterMethodCreator, EmitSubroutine, SubroutineParser, StatementParser, RuntimeCode, WarningFlags, and within ScopedSymbolTable itself).
The absolute µs numbers are inflated because the machine was under load (Spotlight
indexing, Gradle daemon). The ratio is the reliable metric. Ratio improved from 11.83× to ~6.7× — a 43% reduction in the overhead gap.
Activity
fglock commentedon Oct 28, 2026
Handoff: eval STRING per-eval overhead reduction (round 1)
Branch:
feature/regex-perf-hot-pathStatus: Build running, changes implemented but not yet verified
What was done
JFR profiling of 1M
eval '1'iterations identified the per-eval ceremony (not parsing) as the bottleneck — 15 µs/eval on PerlOnJava vs <1 µs on system Perl. The overhead is dominated by object allocation, map construction, and repeated computation of unchanged values.Six optimizations implemented targeting the top JFR hotspots:
IdentityHashMapwithint[]for source-line offsetsParser.javagetWarningBitsString()onScopedSymbolTableScopedSymbolTable.javaBitSet.clone()for empty BitSets inenterScope()ScopedSymbolTable.java%^Hsave/restore when emptyRuntimeCode.java,EvalStringHandler.javaEmitterContext.contextCacheEmitterContext.javaJavaClassInfopin/literal mapsJavaClassInfo.javaNot implemented yet (deferred due to complexity/risk ratio):
BytecodeCompiler(~12 collections, 20+ usage sites). Would eliminate 254 ArrayDeque + 142 HashSet allocations per eval. Safe but needs careful null-checking at all read/write sites.JFR profiling method
JFR tool at:
/Users/fglock/Library/Java/JavaVirtualMachines/temurin-24.0.2/Contents/Home/bin/jfrNext steps
makepasses (build running)dev/bench/benchmark_eval_string.pland compare with system PerlStack(synchronizedVector) withArrayListinScopedSymbolTableperf: reduce per-eval allocation overhead (issue #1911)
perf: eliminate stat64 syscall and cache warning bits (issue #1911)
fglock commentedon Oct 29, 2026
Eval STRING Performance Results (Round 1)
Three commits on
feature/regex-perf-hot-pathreduce per-eval overhead:Changes
Reduce allocation overhead (
07d784b)IdentityHashMap→int[]for source line offsets%^Hdeep copy when emptyEliminate stat64 syscall + cache warning bits (
081b59a)InterpretedCode.shouldKeepRuntimeDeparseSource()calledFile.isFile()on every eval — 16% of CPU doing a filesystem stat on(eval N)names that are never real filesWarningFlags.toWarningBitsString()iterated ConcurrentHashMap (~40 entries) twice per eval (~12.5% of CPU) — add last-result cacheShort-circuit getDisabledWarningCategories (
4f1b3e5)Collections.emptySet()when disabled-warnings BitSet is empty (the common case)Benchmarks
Micro-benchmark (
eval '1'):Full benchmark (
benchmark_eval_string.pl):Profiling method
Used async-profiler 4.5 (
-e cpu) for CPU sampling and JFR for allocation profiling. The stat64 hotspot was invisible in JFR — async-profiler caught it because it profiles native frames.Remaining hotspots (for future work)
ScopedSymbolTable.snapShot()+ constructor: ~14 synchronizedStack.push/popper evalBytecodeCompilerconstruction: 12+ goto-related collections allocated eagerlyWarningBitsRegistry.register(): string concat + ConcurrentHashMap.put per evalWarningFlags.toWarningBitsStringcache depends on external mutable state — the per-ScopedSymbolTable cache was dropped becauseWarningFlags.getCustomWarningCategories()can change without any SST method being calledAll tests pass (
make).perf: add interpreter eval cache for repeated eval STRING (issue #1911)
fglock commentedon Oct 29, 2026
State: handed-off
Agent/session: Codex session 2026-10-29T15:53Z #1926-UAT, resumed 2026-10-29
Scope: #1911; restore
op/attrs.tandop/const-optree.tbehavior in the current checkout after local UAT still showed the regressions.Work: current branch
feature/regex-perf-hot-path, committed fixdc3be933e(fix: avoid context-sensitive eval cache hits (#1911)). Related open PR #1926 already contains the same source guard and focused regression coverage at head9e560c09b87121756eab98e4fb4918f4de3c0522.Completed: The current checkout no longer caches arbitrary eval source. It admits only short decimal integer literals, so evals with compile-time effects or runtime package name resolution are parsed again. Added focused coverage for repeated
use attributesvalidation against changing reference types and dynamic ambiguoustimedispatch. Added a changelog entry. The fix is committed locally, and the worktree is clean.Evidence on current source
dc3be933e:timeout 60 perl src/test/resources/unit/issue_1926_eval_cache_context.tpasses 4/4;timeout 120 ./jperl ...andtimeout 120 ./jperl --interpreter ...each pass 4/4;nice -n 19 timeout 1800 makepasses;nice -n 19 timeout 600 make check-linkspasses;timeout 1200 perl dev/tools/perl_test_runner.pl --jperl ./jperl perl5_t/t/op/attrs.t perl5_t/t/op/const-optree.tpasses 307/307 (159/159 and 148/148). The original failing counts were 147/159 and 147/148.Blockers: none. The matching fix is already on open PR #1926; this checkout's validated commit is ready for the user's next branch/PR action. No merge or additional PR was performed.
Merge pull request #1926 from fglock/feature/issue-1911-eval-string
fglock commentedon Oct 30, 2026
eval STRING benchmark — statistically significant baseline (10 trials × 200k iterations)
Measured with
dev/bench/benchmark_eval_string_stats.plon master (f4f4962).The "cached" scenario shows the interpreter eval cache helps, but per-eval ceremony (compilation lock, scope save/restore, hint hash, BEGIN aliases) still dominates. Next round targets those allocations.
fglock commentedon Oct 30, 2026
warningBitsString cache — benchmark results
Commit: c0832ed57 on
feature/eval-perf-round2What changed
ScopedSymbolTable.getWarningBitsString()was the #1 CPU hotspot in JFR profiles(~22% of CPU time during eval loops). It iterates a
ConcurrentHashMaptwice onevery call. Result depends only on the current scope's warning BitSets, so it's
safe to cache and invalidate on mutation.
Added
cachedWarningBitsStringfield, invalidated in all 12 external mutation sitesacross 10 files (
BytecodeCompiler,EmitCompilerFlag,EvalStringHandler,EmitterMethodCreator,EmitSubroutine,SubroutineParser,StatementParser,RuntimeCode,WarningFlags, and withinScopedSymbolTableitself).Benchmark (1M
eval '1'iterations, paired runs)The absolute µs numbers are inflated because the machine was under load (Spotlight
indexing, Gradle daemon). The ratio is the reliable metric. Ratio improved from
11.83× to ~6.7× — a 43% reduction in the overhead gap.
Combined with prior commits on this branch
All unit tests pass (
makegreen).perf: reduce repeated eval STRING overhead (#1911) (#1931)