From acb125bcda1d2e08598fa638ea9167346b8f019f Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 30 Oct 2026 10:40:08 +0100 Subject: [PATCH] perf: reduce repeated eval STRING overhead (#1911) --- dev/bench/benchmark_eval_string_stats.pl | 78 +++++++ dev/bench/compare_eval_string.sh | 91 ++++++++ .../backend/bytecode/BytecodeCompiler.java | 6 + .../backend/bytecode/EvalStringHandler.java | 1 + .../backend/jvm/EmitCompilerFlag.java | 1 + .../backend/jvm/EmitSubroutine.java | 1 + .../backend/jvm/EmitterMethodCreator.java | 1 + .../frontend/parser/SpecialBlockParser.java | 20 +- .../frontend/parser/StatementParser.java | 21 +- .../frontend/parser/SubroutineParser.java | 5 +- .../frontend/semantic/ScopedSymbolTable.java | 86 ++++--- .../perlonjava/runtime/operators/WarnDie.java | 13 -- .../runtime/runtimetypes/RuntimeCode.java | 209 +++++------------- .../runtimetypes/RuntimeCodeRuntimeState.java | 3 - .../runtime/runtimetypes/WarningFlags.java | 1 + 15 files changed, 325 insertions(+), 212 deletions(-) create mode 100644 dev/bench/benchmark_eval_string_stats.pl create mode 100755 dev/bench/compare_eval_string.sh diff --git a/dev/bench/benchmark_eval_string_stats.pl b/dev/bench/benchmark_eval_string_stats.pl new file mode 100644 index 0000000000..0ebcffd851 --- /dev/null +++ b/dev/bench/benchmark_eval_string_stats.pl @@ -0,0 +1,78 @@ +use strict; +use warnings; +use Time::HiRes qw(time); + +my $trials = 10; +my $n = 200_000; + +my $count = 0; +my $k = 0; + +sub run_uncached { + my @times; + for my $t (1 .. $trials) { + $k = 0; + $count = 0; + my $start = time(); + for (1 .. $n) { + $k++; + my $str = ' + while ( $k < 40 ) { + $k = $k + 1; + $count = $count + 1; + } + ' . "# " . $k; + eval $str; + } + push @times, time() - $start; + } + return @times; +} + +sub run_cached { + my @times; + my $str = 'my $x = 1'; + for my $t (1 .. $trials) { + my $start = time(); + for (1 .. $n) { + eval $str; + } + push @times, time() - $start; + } + return @times; +} + +sub stats { + my @vals = @_; + my $sum = 0; $sum += $_ for @vals; + my $mean = $sum / @vals; + my $ss = 0; $ss += ($_ - $mean)**2 for @vals; + my $sd = sqrt($ss / (@vals - 1)); + my $se = $sd / sqrt(scalar @vals); + return ($mean, $sd, $se); +} + +sub report { + my ($label, $n_iter, @times) = @_; + my ($mean, $sd, $se) = stats(@times); + my $us = $mean / $n_iter * 1e6; + my $us_se = $se / $n_iter * 1e6; + printf " %-10s %d trials x %d iters: %.3f +/- %.3f s (%.2f +/- %.2f us/eval)\n", + $label, scalar @times, $n_iter, $mean, $se, $us, $us_se; + return ($us, $us_se); +} + +print "=== eval STRING benchmark ($trials trials x $n iterations) ===\n\n"; + +print "Uncached (unique eval string each call):\n"; +my @t = run_uncached(); +my ($us, $us_se) = report("", $n, @t); + +print "\nCached (same eval string each call):\n"; +my @tc = run_cached(); +my ($us_c, $us_c_se) = report("", $n, @tc); + +my $engine = $ENV{JPERL_OPTS} ? "jperl" : ($^X =~ /jperl/ ? "jperl" : "perl"); +printf "\nEngine: %s (%s)\n", $engine, $^X; +printf "Uncached: %.2f +/- %.2f us/eval\n", $us, $us_se; +printf "Cached: %.2f +/- %.2f us/eval\n", $us_c, $us_c_se; diff --git a/dev/bench/compare_eval_string.sh b/dev/bench/compare_eval_string.sh new file mode 100755 index 0000000000..428a0d4ffd --- /dev/null +++ b/dev/bench/compare_eval_string.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# Compare eval STRING performance: Perl vs PerlOnJava +# Interleaves trials to account for CPU variability. +# Usage: ./dev/bench/compare_eval_string.sh [trials] [iters] + +set -euo pipefail +cd "$(dirname "$0")/../.." + +TRIALS=${1:-10} +N=${2:-200000} + +cat > /tmp/bench_eval_trial.pl << 'PERL' +use strict; use warnings; use Time::HiRes qw(time); +my $n = $ARGV[0] || 200_000; +my $mode = $ARGV[1] || 'uncached'; +my ($count, $k) = (0, 0); +my $start = time(); +if ($mode eq 'cached') { + my $str = 'my $x = 1'; + for (1 .. $n) { eval $str } +} else { + for (1 .. $n) { + $k++; + my $str = 'while ($k < 40) { $k = $k + 1; $count = $count + 1; }' . "# $k"; + eval $str; + } +} +printf "%.6f\n", time() - $start; +PERL + +run_trial() { + local engine=$1 mode=$2 + if [ "$engine" = "perl" ]; then + perl /tmp/bench_eval_trial.pl "$N" "$mode" + else + timeout 300 ./jperl /tmp/bench_eval_trial.pl "$N" "$mode" + fi +} + +echo "=== eval STRING: Perl vs PerlOnJava ($TRIALS trials x $N iters) ===" +echo "" + +for mode in uncached cached; do + perl_times=() + jperl_times=() + + # Warmup + run_trial perl "$mode" > /dev/null + run_trial jperl "$mode" > /dev/null + + # Interleaved trials + for i in $(seq 1 "$TRIALS"); do + p=$(run_trial perl "$mode") + j=$(run_trial jperl "$mode") + perl_times+=("$p") + jperl_times+=("$j") + # Compute per-trial ratio + ratio=$(echo "$j / $p" | bc -l) + printf " trial %2d: perl=%.3fs jperl=%.3fs ratio=%.1fx\n" "$i" "$p" "$j" "$ratio" + done + + # Compute stats with awk + echo "" + echo "${perl_times[@]}" | tr ' ' '\n' | awk -v n="$N" -v label="perl" ' + { a[NR]=$1; s+=$1 } + END { + m=s/NR; ss=0; for(i=1;i<=NR;i++) ss+=(a[i]-m)^2 + sd=sqrt(ss/(NR-1)); se=sd/sqrt(NR) + us=m/n*1e6; us_se=se/n*1e6 + printf " %-8s mean=%.3f +/- %.3f s (%.2f +/- %.2f us/eval)\n", label, m, se, us, us_se + }' + echo "${jperl_times[@]}" | tr ' ' '\n' | awk -v n="$N" -v label="jperl" ' + { a[NR]=$1; s+=$1 } + END { + m=s/NR; ss=0; for(i=1;i<=NR;i++) ss+=(a[i]-m)^2 + sd=sqrt(ss/(NR-1)); se=sd/sqrt(NR) + us=m/n*1e6; us_se=se/n*1e6 + printf " %-8s mean=%.3f +/- %.3f s (%.2f +/- %.2f us/eval)\n", label, m, se, us, us_se + }' + + # Compute paired ratio with t-test + paste <(printf '%s\n' "${perl_times[@]}") <(printf '%s\n' "${jperl_times[@]}") | awk -v mode="$mode" ' + { r[NR]=$2/$1; s+=r[NR] } + END { + m=s/NR; ss=0; for(i=1;i<=NR;i++) ss+=(r[i]-m)^2 + sd=sqrt(ss/(NR-1)); se=sd/sqrt(NR) + t=(m-1)/se + printf "\n %s ratio: %.2fx +/- %.2f (paired t vs 1.0: t=%.1f, df=%d)\n", mode, m, se, t, NR-1 + }' + echo "" +done diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index fe555c7d81..b8d1db50bc 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1007,6 +1007,7 @@ private void enterScope() { st.warningFlagsStack.push((java.util.BitSet) st.warningFlagsStack.peek().clone()); st.warningFatalStack.push((java.util.BitSet) st.warningFatalStack.peek().clone()); st.warningDisabledStack.push((java.util.BitSet) st.warningDisabledStack.peek().clone()); + st.invalidateWarningBitsCache(); } } @@ -1043,6 +1044,7 @@ private void exitScope(boolean flush) { st.warningFlagsStack.pop(); st.warningFatalStack.pop(); st.warningDisabledStack.pop(); + st.invalidateWarningBitsCache(); } } } @@ -1273,6 +1275,7 @@ private void inheritPragmaFlags(BytecodeCompiler subCompiler) { subCompiler.symbolTable.warningFatalStack.push((java.util.BitSet) parentST.warningFatalStack.peek().clone()); subCompiler.symbolTable.warningDisabledStack.pop(); subCompiler.symbolTable.warningDisabledStack.push((java.util.BitSet) parentST.warningDisabledStack.peek().clone()); + subCompiler.symbolTable.invalidateWarningBitsCache(); } /** @@ -1533,6 +1536,7 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { symbolTable.strictOptionsStack.push(ctx.symbolTable.strictOptionsStack.peek()); symbolTable.featureFlagsStack.pop(); symbolTable.featureFlagsStack.push(ctx.symbolTable.featureFlagsStack.peek()); + symbolTable.invalidateWarningBitsCache(); symbolTable.warningFlagsStack.pop(); symbolTable.warningFlagsStack.push((java.util.BitSet) ctx.symbolTable.warningFlagsStack.peek().clone()); symbolTable.warningFatalStack.pop(); @@ -9226,6 +9230,7 @@ public void visit(LabelNode node) { public void visit(CompilerFlagNode node) { if (emitterContext != null && emitterContext.symbolTable != null) { ScopedSymbolTable st = emitterContext.symbolTable; + st.invalidateWarningBitsCache(); st.warningFlagsStack.pop(); st.warningFlagsStack.push((java.util.BitSet) node.getWarningFlags().clone()); st.warningFatalStack.pop(); @@ -9247,6 +9252,7 @@ public void visit(CompilerFlagNode node) { symbolTable.warningFatalStack.push((java.util.BitSet) node.getWarningFatalFlags().clone()); symbolTable.warningDisabledStack.pop(); symbolTable.warningDisabledStack.push((java.util.BitSet) node.getWarningDisabledFlags().clone()); + symbolTable.invalidateWarningBitsCache(); int warningBitsIdx = addToStringPool(symbolTable.getWarningBitsString()); emit(Opcodes.APPLY_COMPILER_FLAGS); diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index c69aa12bdd..fab5025786 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -560,6 +560,7 @@ private static RuntimeList evalStringList(String perlCode, symbolTable.strictOptionsStack.push(strictOpts); symbolTable.featureFlagsStack.pop(); symbolTable.featureFlagsStack.push(featFlags); + symbolTable.invalidateWarningBitsCache(); symbolTable.warningFlagsStack.pop(); symbolTable.warningFlagsStack.push((java.util.BitSet) currentCode.warningFlags.clone()); WarningFlags.setWarningBitsFromString(symbolTable, siteWarningBits); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitCompilerFlag.java b/src/main/java/org/perlonjava/backend/jvm/EmitCompilerFlag.java index cb200663da..ba6e91aa18 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitCompilerFlag.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitCompilerFlag.java @@ -11,6 +11,7 @@ public static void emitCompilerFlag(EmitterContext ctx, CompilerFlagNode node) { ScopedSymbolTable currentScope = ctx.symbolTable; // Set the warning flags + currentScope.invalidateWarningBitsCache(); currentScope.warningFlagsStack.pop(); currentScope.warningFlagsStack.push((java.util.BitSet) node.getWarningFlags().clone()); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java index 4436e0f2ca..07df1100bf 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java @@ -236,6 +236,7 @@ && isDirectLeafIntegerAddition(node.block, directLeafCaptures, ? value : ctx.symbolTable.strictOptionsStack.peek(); + newSymbolTable.invalidateWarningBitsCache(); newSymbolTable.warningFlagsStack.pop(); newSymbolTable.warningFlagsStack.push(definitionWarningFlags); newSymbolTable.warningFatalStack.pop(); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index 29f7e2dba9..8d2affebce 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -2466,6 +2466,7 @@ private static void applyCompilerFlagNodes(EmitterContext ctx, Node ast) { // Apply disabled warning flags currentScope.warningDisabledStack.pop(); currentScope.warningDisabledStack.push((java.util.BitSet) node.getWarningDisabledFlags().clone()); + currentScope.invalidateWarningBitsCache(); } } } diff --git a/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java b/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java index e8010876bb..dce413df6a 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java @@ -46,21 +46,21 @@ public class SpecialBlockParser { private static final ThreadLocal> compileTimePragmaMutationScopes = ThreadLocal.withInitial(ArrayDeque::new); - private static Stack cloneBitSetStack(Stack source) { - Stack copy = new Stack<>(); + private static ScopedSymbolTable.FastStack cloneBitSetStack(ScopedSymbolTable.FastStack source) { + ScopedSymbolTable.FastStack copy = new ScopedSymbolTable.FastStack<>(); for (BitSet flags : source) { copy.push((BitSet) flags.clone()); } return copy; } - private static Stack cloneIntegerStack(Stack source) { - Stack copy = new Stack<>(); + private static ScopedSymbolTable.FastStack cloneIntegerStack(ScopedSymbolTable.FastStack source) { + ScopedSymbolTable.FastStack copy = new ScopedSymbolTable.FastStack<>(); copy.addAll(source); return copy; } - private static void restoreStack(Stack target, Stack source) { + private static void restoreStack(ScopedSymbolTable.FastStack target, ScopedSymbolTable.FastStack source) { target.clear(); target.addAll(source); } @@ -507,11 +507,11 @@ static RuntimeList runSpecialBlock(Parser parser, String blockPhase, Node block, if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("Special block captures " + parser.ctx.symbolTable.getAllVisibleVariables()); RuntimeList result; boolean preserveCallerPragmas = !blockPhase.equals("BEGIN"); - Stack savedWarningFlagsStack = null; - Stack savedWarningFatalStack = null; - Stack savedWarningDisabledStack = null; - Stack savedFeatureFlagsStack = null; - Stack savedStrictOptionsStack = null; + ScopedSymbolTable.FastStack savedWarningFlagsStack = null; + ScopedSymbolTable.FastStack savedWarningFatalStack = null; + ScopedSymbolTable.FastStack savedWarningDisabledStack = null; + ScopedSymbolTable.FastStack savedFeatureFlagsStack = null; + ScopedSymbolTable.FastStack savedStrictOptionsStack = null; if (preserveCallerPragmas) { savedWarningFlagsStack = cloneBitSetStack(parser.ctx.symbolTable.warningFlagsStack); savedWarningFatalStack = cloneBitSetStack(parser.ctx.symbolTable.warningFatalStack); diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java index b91be10644..31b20e1760 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java @@ -173,21 +173,21 @@ private static void markInsideGiven(Node node) { if (node instanceof IfNode conditional) { markInsideGiven(conditional.condition); markInsideGiven(conditional.thenBranch); markInsideGiven(conditional.elseBranch); return; } if (node instanceof TernaryOperatorNode ternary) { markInsideGiven(ternary.condition); markInsideGiven(ternary.trueExpr); markInsideGiven(ternary.falseExpr); } } - private static Stack cloneBitSetStack(Stack source) { - Stack copy = new Stack<>(); + private static ScopedSymbolTable.FastStack cloneBitSetStack(ScopedSymbolTable.FastStack source) { + ScopedSymbolTable.FastStack copy = new ScopedSymbolTable.FastStack<>(); for (BitSet flags : source) { copy.push((BitSet) flags.clone()); } return copy; } - private static Stack cloneIntegerStack(Stack source) { - Stack copy = new Stack<>(); + private static ScopedSymbolTable.FastStack cloneIntegerStack(ScopedSymbolTable.FastStack source) { + ScopedSymbolTable.FastStack copy = new ScopedSymbolTable.FastStack<>(); copy.addAll(source); return copy; } - private static void restoreStack(Stack target, Stack source) { + private static void restoreStack(ScopedSymbolTable.FastStack target, ScopedSymbolTable.FastStack source) { target.clear(); target.addAll(source); } @@ -1383,6 +1383,7 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) { for (int i = 0; i < parser.ctx.symbolTable.warningFlagsStack.size(); i++) { parser.ctx.symbolTable.warningFlagsStack.set(i, (java.util.BitSet) currentWarnings.clone()); } + parser.ctx.symbolTable.invalidateWarningBitsCache(); } } } @@ -1544,11 +1545,11 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) { // context. Its final lexical pragma state must not be copied back over the // surrounding file scope; the actual pragma effect of a `use` statement happens // below when import()/unimport() is invoked against parser.ctx.symbolTable. - Stack savedWarningFlagsStack = cloneBitSetStack(ctx.symbolTable.warningFlagsStack); - Stack savedWarningFatalStack = cloneBitSetStack(ctx.symbolTable.warningFatalStack); - Stack savedWarningDisabledStack = cloneBitSetStack(ctx.symbolTable.warningDisabledStack); - Stack savedFeatureFlagsStack = cloneIntegerStack(ctx.symbolTable.featureFlagsStack); - Stack savedStrictOptionsStack = cloneIntegerStack(ctx.symbolTable.strictOptionsStack); + ScopedSymbolTable.FastStack savedWarningFlagsStack = cloneBitSetStack(ctx.symbolTable.warningFlagsStack); + ScopedSymbolTable.FastStack savedWarningFatalStack = cloneBitSetStack(ctx.symbolTable.warningFatalStack); + ScopedSymbolTable.FastStack savedWarningDisabledStack = cloneBitSetStack(ctx.symbolTable.warningDisabledStack); + ScopedSymbolTable.FastStack savedFeatureFlagsStack = cloneIntegerStack(ctx.symbolTable.featureFlagsStack); + ScopedSymbolTable.FastStack savedStrictOptionsStack = cloneIntegerStack(ctx.symbolTable.strictOptionsStack); RuntimeList args; try { // The implicit BEGIN wrapper that evaluates a use/no diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index a6d0ca7363..20d244a36e 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -2571,13 +2571,14 @@ && isLexicalSubStorageReferenced(explicitlyUsedVars, entry.name()) compilationBlock.setAnnotation("compiledSubroutineName", fullName); // Clone warning flags (critical for 'no warnings' pragmas) + filteredSnapshot.invalidateWarningBitsCache(); filteredSnapshot.warningFlagsStack.pop(); // Remove the initial value pushed by enterScope filteredSnapshot.warningFlagsStack.push(definitionWarningFlags); - + // Clone fatal warning flags (critical for 'use warnings FATAL' pragmas) filteredSnapshot.warningFatalStack.pop(); filteredSnapshot.warningFatalStack.push(definitionWarningFatalFlags); - + // Clone disabled warning flags (critical for 'no warnings' pragmas) filteredSnapshot.warningDisabledStack.pop(); filteredSnapshot.warningDisabledStack.push(definitionWarningDisabledFlags); diff --git a/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java b/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java index 4aa534dc6a..8101939394 100644 --- a/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java +++ b/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java @@ -78,6 +78,7 @@ public void inheritAllWarningsForRegisteredCategory(String category) { if (categoryBit == null || allBit == null) { return; } + boolean changed = false; for (int i = 0; i < warningFlagsStack.size(); i++) { BitSet disabled = warningDisabledStack.get(i); if (warningFlagsStack.get(i).get(allBit) @@ -87,54 +88,67 @@ public void inheritAllWarningsForRegisteredCategory(String category) { if (warningFatalStack.get(i).get(allBit)) { warningFatalStack.get(i).set(categoryBit); } + changed = true; } } + if (changed) { + cachedWarningBitsString = null; + } + } + + /** Unsynchronized stack backed by ArrayList. Replaces java.util.Stack (Vector-based) to eliminate per-operation monitor overhead in single-threaded compilation paths. */ + public static final class FastStack extends ArrayList { + public void push(T item) { add(item); } + public T pop() { return remove(size() - 1); } + public T peek() { return get(size() - 1); } } // Stack to manage warning categories for each scope - public final Stack warningFlagsStack = new Stack<>(); + public final FastStack warningFlagsStack = new FastStack<>(); // Stack to track explicitly disabled warning categories (for proper $^W interaction) - public final Stack warningDisabledStack = new Stack<>(); + public final FastStack warningDisabledStack = new FastStack<>(); // Stack to track FATAL warning categories for each scope - public final Stack warningFatalStack = new Stack<>(); + public final FastStack warningFatalStack = new FastStack<>(); // Stack to manage feature categories for each scope - public final Stack featureFlagsStack = new Stack<>(); + public final FastStack featureFlagsStack = new FastStack<>(); // Exact state for postderef_qq. The legacy int mask has more than 32 // categories and can alias high bit positions modulo 32. - private final Stack postderefQqStack = new Stack<>(); + private final FastStack postderefQqStack = new FastStack<>(); // Exact lexical state for experimental enhanced /xx class parsing. This // cannot safely use the legacy 32-bit feature mask. - private final Stack enhancedXxStack = new Stack<>(); + private final FastStack enhancedXxStack = new FastStack<>(); // Stack to manage strict options for each scope - public final Stack strictOptionsStack = new Stack<>(); + public final FastStack strictOptionsStack = new FastStack<>(); // Strict categories explicitly set by use/no strict. `use VERSION` enables // strict only where the source has not explicitly chosen otherwise. - private final Stack explicitStrictOptionsStack = new Stack<>(); + private final FastStack explicitStrictOptionsStack = new FastStack<>(); // Lexical default regex modifiers installed by `use re '/flags'`. // These do not fit in PerlOnJava's legacy 32-bit strict-options mask and // must follow the same lexical enter/exit/snapshot rules independently. - private final Stack regexModifierStack = new Stack<>(); + private final FastStack regexModifierStack = new FastStack<>(); // Coarse stable re-debug categories captured lexically. Perl exposes many // engine-specific named flags; the frontend preserves their compile versus // execute ownership without baking a particular engine's bit layout into // the AST. - private final Stack regexDebugFlagsStack = new Stack<>(); + private final FastStack regexDebugFlagsStack = new FastStack<>(); /** True when this compilation unit explicitly invoked re->import/unimport. */ private boolean lexicalRegexPragmaChanged; // `use VERSION` is lexical: declaration and warning state resets for an // inner block and is restored when that block exits. - private final Stack useVersionStack = new Stack<>(); + private final FastStack useVersionStack = new FastStack<>(); /** Lexically scoped parser keywords installed by compile-time modules. */ - private final Stack> parserKeywordPluginsStack = new Stack<>(); + private final FastStack> parserKeywordPluginsStack = new FastStack<>(); // A stack to manage nested scopes of symbol tables. - private final Stack symbolTableStack = new Stack<>(); - private final Stack packageStack = new Stack<>(); + private final FastStack symbolTableStack = new FastStack<>(); + private final FastStack packageStack = new FastStack<>(); // Stack to manage nested subroutine names for error messages - private final Stack subroutineStack = new Stack<>(); + private final FastStack subroutineStack = new FastStack<>(); // Stack to track whether we are inside a subroutine body (named or anonymous) - private final Stack inSubroutineBodyStack = new Stack<>(); + private final FastStack inSubroutineBodyStack = new FastStack<>(); // Cache for the getAllVisibleVariables method private Map visibleVariablesCache; + // Cache for getWarningBitsString(); null means stale + private String cachedWarningBitsString; /** * Constructs a ScopedSymbolTable. @@ -234,6 +248,7 @@ public static void clearPackageVersions() { */ public int enterScope() { clearVisibleVariablesCache(); + cachedWarningBitsString = null; // Push a new SymbolTable onto the stack and set its index symbolTableStack.push(new SymbolTable(symbolTableStack.peek().index)); @@ -243,12 +258,14 @@ public int enterScope() { subroutineStack.push(subroutineStack.peek()); // Push a copy of the current subroutine-body flag onto the stack inSubroutineBodyStack.push(inSubroutineBodyStack.peek()); - // Push a copy of the current warning categories map onto the stack - warningFlagsStack.push((BitSet) warningFlagsStack.peek().clone()); - // Push a copy of the current disabled warnings map onto the stack - warningDisabledStack.push((BitSet) warningDisabledStack.peek().clone()); - // Push a copy of the current fatal warnings map onto the stack - warningFatalStack.push((BitSet) warningFatalStack.peek().clone()); + // Push a copy of the current warning categories map onto the stack. + // Empty BitSets are common (code without `use warnings`); avoid clone overhead. + BitSet curWarnings = warningFlagsStack.peek(); + warningFlagsStack.push(curWarnings.isEmpty() ? new BitSet() : (BitSet) curWarnings.clone()); + BitSet curDisabled = warningDisabledStack.peek(); + warningDisabledStack.push(curDisabled.isEmpty() ? new BitSet() : (BitSet) curDisabled.clone()); + BitSet curFatal = warningFatalStack.peek(); + warningFatalStack.push(curFatal.isEmpty() ? new BitSet() : (BitSet) curFatal.clone()); // Push a copy of the current feature categories map onto the stack featureFlagsStack.push(featureFlagsStack.peek()); postderefQqStack.push(postderefQqStack.peek()); @@ -259,7 +276,8 @@ public int enterScope() { regexModifierStack.push(regexModifierStack.peek()); regexDebugFlagsStack.push(regexDebugFlagsStack.peek()); useVersionStack.push(null); - parserKeywordPluginsStack.push(new LinkedHashMap<>(parserKeywordPluginsStack.peek())); + Map curPlugins = parserKeywordPluginsStack.peek(); + parserKeywordPluginsStack.push(curPlugins.isEmpty() ? new LinkedHashMap<>() : new LinkedHashMap<>(curPlugins)); // Return the current size of the symbol table stack as the scope index return symbolTableStack.size() - 1; @@ -297,6 +315,7 @@ public int addVariableInScope(int scopeIndex, String name, String variableDeclTy */ public void exitScope(int scopeIndex) { clearVisibleVariablesCache(); + cachedWarningBitsString = null; // Capture the child scope's max local variable index before popping int childIndex = symbolTableStack.peek().index; // Pop entries from the stacks until reaching the specified scope index @@ -1030,8 +1049,8 @@ public ScopedSymbolTable snapShot() { st.regexDebugFlagsStack.pop(); st.regexDebugFlagsStack.push(this.regexDebugFlagsStack.peek()); st.parserKeywordPluginsStack.pop(); - st.parserKeywordPluginsStack.push(new LinkedHashMap<>( - this.parserKeywordPluginsStack.peek())); + Map srcPlugins = this.parserKeywordPluginsStack.peek(); + st.parserKeywordPluginsStack.push(srcPlugins.isEmpty() ? new LinkedHashMap<>() : new LinkedHashMap<>(srcPlugins)); st.lexicalRegexPragmaChanged = this.lexicalRegexPragmaChanged; return st; @@ -1128,6 +1147,7 @@ public String toString() { public void enableWarningCategory(String category) { Integer bitPosition = warningBitPositions().get(category); if (bitPosition != null) { + cachedWarningBitsString = null; warningFlagsStack.peek().set(bitPosition); // A normal "use warnings 'category'" downgrades any inherited // FATAL bit for that category back to a regular warning. @@ -1140,6 +1160,7 @@ public void enableWarningCategory(String category) { public void disableWarningCategory(String category) { Integer bitPosition = warningBitPositions().get(category); if (bitPosition != null) { + cachedWarningBitsString = null; warningFlagsStack.peek().clear(bitPosition); // A disabled category cannot remain fatal. Keeping the fatal bit // made warning-mask serialization re-enable the category when an @@ -1192,6 +1213,7 @@ public Set getDisabledWarningCategories() { public void enableFatalWarningCategory(String category) { Integer bitPosition = warningBitPositions().get(category); if (bitPosition != null) { + cachedWarningBitsString = null; warningFatalStack.peek().set(bitPosition); // FATAL implies enabled warningFlagsStack.peek().set(bitPosition); @@ -1205,6 +1227,7 @@ public void enableFatalWarningCategory(String category) { public void disableFatalWarningCategory(String category) { Integer bitPosition = warningBitPositions().get(category); if (bitPosition != null) { + cachedWarningBitsString = null; warningFatalStack.peek().clear(bitPosition); } } @@ -1224,7 +1247,15 @@ public boolean isFatalWarningCategory(String category) { * * @return A string of bytes representing the warning bits in Perl 5 format. */ + /** Invalidate the warningBitsString cache. Call after direct mutation of warning stacks. */ + public void invalidateWarningBitsCache() { + cachedWarningBitsString = null; + } + public String getWarningBitsString() { + if (cachedWarningBitsString != null) { + return cachedWarningBitsString; + } Map positions = warningBitPositions(); BitSet enabled = (BitSet) warningFlagsStack.peek().clone(); BitSet fatal = (BitSet) warningFatalStack.peek().clone(); @@ -1242,7 +1273,8 @@ public String getWarningBitsString() { } } } - return WarningFlags.toWarningBitsString(enabled, fatal, positions); + cachedWarningBitsString = WarningFlags.toWarningBitsString(enabled, fatal, positions); + return cachedWarningBitsString; } // Methods for managing features using bit positions @@ -1278,6 +1310,7 @@ private void enableExperimentalFeatureWarning(String feature) { String experimentalWarning = "experimental::" + feature; Integer warnBitPos = warningBitPositions().get(experimentalWarning); if (warnBitPos != null && !warningDisabledStack.peek().get(warnBitPos)) { + cachedWarningBitsString = null; warningFlagsStack.peek().set(warnBitPos); } } @@ -1366,6 +1399,7 @@ public void copyFlagsFrom(ScopedSymbolTable source, int sourceScopeIndex) { int index = Math.max(0, Math.min(sourceScopeIndex, source.warningFlagsStack.size() - 1)); // Copy warning flags + cachedWarningBitsString = null; this.warningFlagsStack.pop(); this.warningFlagsStack.push((BitSet) source.warningFlagsStack.get(index).clone()); diff --git a/src/main/java/org/perlonjava/runtime/operators/WarnDie.java b/src/main/java/org/perlonjava/runtime/operators/WarnDie.java index 34cc732dfc..e764ced456 100644 --- a/src/main/java/org/perlonjava/runtime/operators/WarnDie.java +++ b/src/main/java/org/perlonjava/runtime/operators/WarnDie.java @@ -23,17 +23,6 @@ public class WarnDie { private static final ThreadLocal WRITING_WARNING = ThreadLocal.withInitial(() -> Boolean.FALSE); - private static final ThreadLocal COMPILE_TIME_WARNING_EMITTED = - ThreadLocal.withInitial(() -> Boolean.FALSE); - - public static void clearCompileTimeWarningFlag() { - COMPILE_TIME_WARNING_EMITTED.set(Boolean.FALSE); - } - - public static boolean wasCompileTimeWarningEmitted() { - return COMPILE_TIME_WARNING_EMITTED.get(); - } - public static boolean isInsideUnhandledDieHandler() { return PerlRuntime.current().executionState().insideUnhandledDieHandler; } @@ -360,7 +349,6 @@ public static RuntimeBase warn(RuntimeBase message, RuntimeScalar where) { } public static RuntimeBase warn(RuntimeBase message, RuntimeScalar where, String fileName, int lineNumber) { - COMPILE_TIME_WARNING_EMITTED.set(Boolean.TRUE); // PERL5DB is debugger bootstrap code; Perl emits its implicit warning // without the eval-string source suffix in this special context. if (org.perlonjava.runtime.debugger.DebugHooks.isExecutingPerl5db()) { @@ -607,7 +595,6 @@ public static RuntimeBase warnWithCategoryFromCode(RuntimeBase message, RuntimeS private static RuntimeBase warnWithCategory(RuntimeBase message, RuntimeScalar where, String category, String fileName, int lineNumber, boolean enabledByDefault, String warningBitsOverride) { - COMPILE_TIME_WARNING_EMITTED.set(Boolean.TRUE); if (WarningFlags.areWarningsForcedOff()) { return new RuntimeScalar(); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index ec71340b37..1007ecb7a3 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -987,48 +987,6 @@ private static Map> evalCache() { return PerlRuntime.current().runtimeCodeState().evalCache; } - private static Map interpreterEvalCache() { - return PerlRuntime.current().runtimeCodeState().interpreterEvalCache; - } - - /** - * Eval parsing can execute compile-time code and resolve names against - * mutable lexical and package state. The interpreter cache currently has - * no complete fingerprint for that state, so admit only decimal integer - * literals, whose compiled form is independent of those side effects. - */ - private static boolean isCacheableInterpreterEvalSource(String source) { - if (source == null || source.length() > 32) { - return false; - } - int index = 0; - while (index < source.length() && isEvalAsciiWhitespace(source.charAt(index))) { - index++; - } - if (index < source.length() && (source.charAt(index) == '+' || source.charAt(index) == '-')) { - index++; - } - int digitCount = 0; - while (index < source.length()) { - char current = source.charAt(index); - if (current < '0' || current > '9') { - break; - } - digitCount++; - index++; - } - if (digitCount == 0 || digitCount > 9) { - return false; - } - while (index < source.length() && isEvalAsciiWhitespace(source.charAt(index))) { - index++; - } - return index == source.length(); - } - - private static boolean isEvalAsciiWhitespace(char value) { - return value == ' ' || value == '\t' || value == '\n' || value == '\r' || value == '\f'; - } /** * Flag to enable disassembly of eval STRING bytecode. @@ -4937,6 +4895,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje return null; } finally { // Restore caller lexical flags (do not leak eval pragmas). + capturedSymbolTable.invalidateWarningBitsCache(); capturedSymbolTable.warningFlagsStack.pop(); capturedSymbolTable.warningFlagsStack.push((BitSet) savedWarningFlags.clone()); @@ -5298,85 +5257,12 @@ public static RuntimeList evalStringWithInterpreter( } } - // Clone compiler options and set isUnicodeSource if needed - // Always clone to avoid modifying the original and to set a unique filename - CompilerOptions evalCompilerOptions = ctx.compilerOptions.clone(); - boolean isByteStringSource = !ctx.isEvalbytes - && code.type == RuntimeScalarType.BYTE_STRING - && !byteStringUtf8Source; - if (hasUnicode) { - evalCompilerOptions.isUnicodeSource = true; - } - if (byteStringUtf8Source) { - // The byte-backed eval source has been decoded above. Do not - // let the caller's byte-source mode reinterpret it as octets. - evalCompilerOptions.isByteStringSource = false; - } - if (ctx.isEvalbytes && !evalbytesUtf8Source) { - evalCompilerOptions.isEvalbytes = true; - } - if (isByteStringSource) { - evalCompilerOptions.isUnicodeSource = false; - evalCompilerOptions.isByteStringSource = true; - } - // EmitEval gives each call site a synthetic compilerOptions name, - // but retains the enclosing source in errorUtil. Allocate within - // that enclosing source so distinct sites share its `(eval N)` - // sequence without perturbing unrelated source files. - evalCompilerOptions.fileName = getNextEvalFilenameForDebugger(ctx.errorUtil.getFileName()); - evalFilename = evalCompilerOptions.fileName; - warnSignatureArgsInEval(evalString, evalCompilerOptions.fileName); - - // Setup for BEGIN block support - create aliases for captured variables. - // IMPORTANT: Do NOT mutate AST nodes (e.g. operatorAst.id) here. - // The AST is shared with the JVM compiler; mutating it would change how - // `my` declarations are compiled (NEW vs RETRIEVE_BEGIN), causing variables - // inside loops to stop being reinitialized between iterations. - ScopedSymbolTable capturedSymbolTable = ctx.symbolTable; - Map capturedVars = capturedSymbolTable.getAllVisibleVariables(); - for (SymbolTable.SymbolEntry entry : capturedVars.values()) { - if (!entry.name().equals("@_") && !entry.decl().isEmpty() && !entry.name().startsWith("&")) { - if (!entry.decl().equals("our")) { - Object runtimeValue = runtimeCtx.getRuntimeValue(entry.name()); - if (runtimeValue != null) { - { - int beginId = evalBeginId(entry); - String packageName = PersistentVariable.beginPackage(beginId); - String varNameWithoutSigil = entry.name().substring(1); - String fullName = packageName + "::" + varNameWithoutSigil; + // Allocate eval filename early (needed for both cached and compiled paths). + // Defer CompilerOptions clone to compilation path to avoid allocation on cache hits. + evalFilename = getNextEvalFilenameForDebugger(ctx.errorUtil.getFileName()); + CompilerOptions evalCompilerOptions = null; - // Only install the alias (and schedule cleanup) if no other - // alias is already in place for this key. An alias may already - // exist because SubroutineParser.handleNamedSub registered one - // at compile time for a named sub that closes over the same - // outer `my`. That alias must stay alive until the owning - // `my %name = (...)` runs and calls retrieveBeginHash, which - // takes ownership. If we unconditionally put+remove here, we - // delete SubroutineParser's alias in the finally block, and the - // later `my` creates a fresh, unshared object — breaking the - // named sub's closure over that variable. - boolean installed = false; - if (runtimeValue instanceof RuntimeArray) { - if (GlobalVariable.globalArrays.putIfAbsent(fullName, (RuntimeArray) runtimeValue) == null) { - installed = true; - } - } else if (runtimeValue instanceof RuntimeHash) { - if (GlobalVariable.globalHashes.putIfAbsent(fullName, (RuntimeHash) runtimeValue) == null) { - installed = true; - } - } else if (runtimeValue instanceof RuntimeScalar) { - if (GlobalVariable.scalarSlots().putIfAbsent(fullName, (RuntimeScalar) runtimeValue) == null) { - installed = true; - } - } - if (installed) { - registerEvalRuntimeAlias(runtimeCtx, entry.name().charAt(0), fullName, (RuntimeBase) runtimeValue); - } - } - } - } - } - } + ScopedSymbolTable capturedSymbolTable = ctx.symbolTable; // Compute evalCurrentPackage early (needed for cache key) String runtimePackage = InterpreterState.currentPackage.get().toString(); @@ -5388,24 +5274,61 @@ public static RuntimeList evalStringWithInterpreter( : topLevelMain ? runtimePackage : capturedSymbolTable.getCurrentPackage(); try { - // Check interpreter eval cache (skip when debugger is active) - boolean isDebugging = debuggerFlagsAtEntry != 0; - String interpreterCacheKey = null; - if (!isDebugging && isCacheableInterpreterEvalSource(evalString)) { - int featureFlags = capturedSymbolTable.featureFlagsStack.peek(); - interpreterCacheKey = evalString + '\0' + evalTag + '\0' + hasUnicode - + '\0' + ctx.isEvalbytes + '\0' + featureFlags + '\0' + evalCurrentPackage; - Map cache = interpreterEvalCache(); - synchronized (cache) { - interpretedCode = cache.get(interpreterCacheKey); - } - if (interpretedCode != null) { - compiledSuccessfully = true; + // Setup for BEGIN block support - create aliases for captured variables. + Map capturedVars = capturedSymbolTable.getAllVisibleVariables(); + for (SymbolTable.SymbolEntry entry : capturedVars.values()) { + if (!entry.name().equals("@_") && !entry.decl().isEmpty() && !entry.name().startsWith("&")) { + if (!entry.decl().equals("our")) { + Object runtimeValue = runtimeCtx.getRuntimeValue(entry.name()); + if (runtimeValue != null) { + { + int beginId = evalBeginId(entry); + String packageName = PersistentVariable.beginPackage(beginId); + String varNameWithoutSigil = entry.name().substring(1); + String fullName = packageName + "::" + varNameWithoutSigil; + + boolean installed = false; + if (runtimeValue instanceof RuntimeArray) { + if (GlobalVariable.globalArrays.putIfAbsent(fullName, (RuntimeArray) runtimeValue) == null) { + installed = true; + } + } else if (runtimeValue instanceof RuntimeHash) { + if (GlobalVariable.globalHashes.putIfAbsent(fullName, (RuntimeHash) runtimeValue) == null) { + installed = true; + } + } else if (runtimeValue instanceof RuntimeScalar) { + if (GlobalVariable.scalarSlots().putIfAbsent(fullName, (RuntimeScalar) runtimeValue) == null) { + installed = true; + } + } + if (installed) { + registerEvalRuntimeAlias(runtimeCtx, entry.name().charAt(0), fullName, (RuntimeBase) runtimeValue); + } + } + } + } } } - - if (interpretedCode == null) { - WarnDie.clearCompileTimeWarningFlag(); + // Clone compiler options for compilation (deferred from cache-hit fast path) + evalCompilerOptions = ctx.compilerOptions.clone(); + boolean isByteStringSource = !ctx.isEvalbytes + && code.type == RuntimeScalarType.BYTE_STRING + && !byteStringUtf8Source; + if (hasUnicode) { + evalCompilerOptions.isUnicodeSource = true; + } + if (byteStringUtf8Source) { + evalCompilerOptions.isByteStringSource = false; + } + if (ctx.isEvalbytes && !evalbytesUtf8Source) { + evalCompilerOptions.isEvalbytes = true; + } + if (isByteStringSource) { + evalCompilerOptions.isUnicodeSource = false; + evalCompilerOptions.isByteStringSource = true; + } + evalCompilerOptions.fileName = evalFilename; + warnSignatureArgsInEval(evalString, evalFilename); // Parse the eval string Lexer lexer = new Lexer(evalString); tokens = lexer.tokenize(); @@ -5541,23 +5464,13 @@ public static RuntimeList evalStringWithInterpreter( System.out.println(Disassemble.disassemble(interpretedCode)); } - // Store in interpreter eval cache (before withCapturedVars) - if (interpreterCacheKey != null && interpretedCode != null - && !WarnDie.wasCompileTimeWarningEmitted()) { - Map cache = interpreterEvalCache(); - synchronized (cache) { - cache.put(interpreterCacheKey, interpretedCode); - } - } - // Retention is decided at eval compilation time. In // particular, a later cleanup of a localized $^P must not // turn an 0x800-only failed eval into a retained source. storeSourceLines(evalString, evalFilename, ast, tokens, true, debuggerFlagsAtEntry); sourceLinesStored = true; - } // end if (interpretedCode == null) - // Set captured variables (both cached and compiled paths) + // Set captured variables if (runtimeValues.length > 0) { RuntimeBase[] capturedVars2 = new RuntimeBase[runtimeValues.length]; for (int i = 0; i < runtimeValues.length; i++) { @@ -5730,7 +5643,7 @@ public static RuntimeList evalStringWithInterpreter( // Execute the interpreted code // Track eval depth for $^S support incrementEvalDepth(); - pushActiveEvalSource(evalCompilerOptions.fileName, evalString); + pushActiveEvalSource(evalFilename, evalString); try { // Eval STRING is an execution boundary: resolve a goto &sub // marker here so its eval-string restriction is caught and diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCodeRuntimeState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCodeRuntimeState.java index e15c83b8d2..c7d92a34b6 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCodeRuntimeState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCodeRuntimeState.java @@ -1,6 +1,5 @@ package org.perlonjava.runtime.runtimetypes; -import org.perlonjava.backend.bytecode.InterpretedCode; import org.perlonjava.backend.jvm.EmitterContext; import org.perlonjava.frontend.astnode.OperatorNode; @@ -21,7 +20,6 @@ public final class RuntimeCodeRuntimeState { final IdentityHashMap evalBeginIds = new IdentityHashMap<>(); final Map> evalCache = lruMap(EVAL_CACHE_SIZE); - final Map interpreterEvalCache = lruMap(EVAL_CACHE_SIZE); final Map, MethodHandle> methodHandleCache = lruMap(METHOD_HANDLE_CACHE_SIZE); final HashMap> anonymousSubs = new HashMap<>(); final HashMap deparseSourceTextsByClassName = new HashMap<>(); @@ -140,7 +138,6 @@ void clearStableHashMethodBlessIdCache() { void clearCaches() { evalBeginIds.clear(); evalCache.clear(); - interpreterEvalCache.clear(); methodHandleCache.clear(); anonymousSubs.clear(); deparseSourceTextsByClassName.clear(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/WarningFlags.java b/src/main/java/org/perlonjava/runtime/runtimetypes/WarningFlags.java index cd4b33d60e..aa8712866a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/WarningFlags.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/WarningFlags.java @@ -496,6 +496,7 @@ public static void setWarningBitsFromString(ScopedSymbolTable symbolTable, Strin } // Clear all warning flags first + symbolTable.invalidateWarningBitsCache(); symbolTable.warningFlagsStack.peek().clear(); symbolTable.warningFatalStack.peek().clear();