Skip to content

perf: skip alias loop and BitSet clone for simple eval strings (issue #1961) - #1978

Open
fglock wants to merge 1 commit into
masterfrom
feature/eval-perf-round4
Open

fglock wants to merge 1 commit into
masterfrom
feature/eval-perf-round4

Conversation

@fglock

@fglock fglock commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to PR #1940 (issue #1911), targeting the remaining eval STRING overhead from issue #1961.

Changes

1. Skip outer-lexical alias loop when not needed (sawBeginOrSub flag)

evalStringWithInterpreter() previously ran an O(visible_vars) loop on every eval to install package-global aliases for outer my variables — required only for BEGIN blocks and named sub declarations (which outlive the eval frame and need package-scoped access to outer lexicals).

Fix: Add public boolean sawBeginOrSub to Lexer. consumeIdentifier() sets it on the first occurrence of BEGIN, sub, use, or no. After tokenize(), the alias loop runs only when sawBeginOrSub is true.

For the benchmark (a while loop with no subs), this eliminates the alias loop entirely.

2. Avoid BitSet.clone() for empty BitSets in snapShot()

ScopedSymbolTable.snapShot() unconditionally cloned three warning-flag BitSets. These are empty in the common case (no use warnings pragmas active). Use new BitSet() instead of clone() when the source is empty — same pattern already applied in enterScope().

Benchmark

dev/bench/benchmark_eval_string.pl, 3-run median on the same machine:

ops/sec
System Perl ~326,000/s
PerlOnJava (post-PR #1940) ~44,000/s
PerlOnJava (this PR) ~48,400/s
Improvement vs PR #1940 ~+10%
Ratio vs system Perl ~6.7×

Testing

  • Full make passes (BUILD SUCCESSFUL)
  • All failures in the test report are pre-existing on master (verified)
  • Fast path verified: eval "while (...) { ... }" correctly executes without alias loop
  • Gated path verified: eval "sub foo { ... }", eval "BEGIN { ... }", eval "use strict" all work correctly

Closes

Addresses items 3 and 4 from issue #1961 (items 1 and 2 were already done in PR #1940; item 5 is not safe because LexerToken fields are mutated by the parser).

🤖 Generated with Claude Code

…strings (issue #1961)

- Add `sawBeginOrSub` flag to `Lexer`: set when `BEGIN`, `sub`, `use`, or `no`
  appears in the token stream.
- Reorder `evalStringWithInterpreter()`: tokenize first (cheap, no side effects),
  then run the O(visible_vars) outer-lexical alias loop only when `sawBeginOrSub`
  is true. Saves per-eval HashMap lookups and String allocations on the common
  path (simple expressions, while loops, etc.).
- Fix `ScopedSymbolTable.snapShot()`: use `new BitSet()` instead of `clone()`
  for the three warning-flag BitSets when they are empty, matching the pattern
  already applied in `enterScope()`.

Benchmark (dev/bench/benchmark_eval_string.pl, 3-run median):
  System Perl:  ~326,000/s
  PerlOnJava:   ~48,400/s  (was ~44,000/s before this PR)
  Ratio:        ~6.7x  (was ~7.4x before PR #1940; ~12x before #1911 work)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant