Skip to content

perf(interp): lazy arguments materialization (−29% alloc on call-heavy code)#1276

Merged
nickna merged 1 commit into
mainfrom
worktree-lazy-arguments
Jul 12, 2026
Merged

perf(interp): lazy arguments materialization (−29% alloc on call-heavy code)#1276
nickna merged 1 commit into
mainfrom
worktree-lazy-arguments

Conversation

@nickna

@nickna nickna commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Round 9b: the top remaining interpreter perf item from the July audits.

Problem

Every interpreter call of a user function unconditionally materialized the JS arguments binding — a List<object?> + SharpTSArray, plus per-arg ToObject() boxing on the CallV2 span path — even though the overwhelming majority of functions never reference it.

Change

  • Hoisted ClosureAnalyzer's private ArgumentsRefScanner to Parsing/Visitors/ArgumentsRefScanner.cs as the single shared source. It already encodes the spec scoping rules: stop at nested non-arrow function boundaries (they bind their own arguments), descend into true arrows (they inherit lexically). The compiled-mode analyzer keeps identical behavior.
  • New ArgumentsUsage cache (ConditionalWeakTable keyed by declaration AST node — function factories scan once per declaration, not per closure) gates all four Define("arguments", …) sites: SharpTSFunction.Call/CallV2 and the function-expression paths.
  • Direct-eval conservatism: interpreter eval runs against the live scope chain, so eval("arguments[0]") can observe the binding without the identifier appearing in the AST. The scanner takes a treatEvalReferenceAsUse option (true for the interpreter gate, false for the compiled analyzer's existing behavior) — any eval reference in a body keeps the materialization.

Measurement

In-process lex+parse+check+interpret loop over a call-heavy script (180k mixed declaration/expression/arrow calls + fib recursion), Release, A/B via detached origin/main checkout, medians of 5:

origin/main this PR delta
allocated / run 175,437 KB 123,882 KB −29.4%
gen0 GCs / 30 runs 573 404 −29.5%
time / run (median) 106.1 ms 94.4 ms −11.0%

Verification

  • Full unit suite: 15452 passed / 0 failed
  • New regression tests: lodash-style function(){ return func.apply(this, arguments); } forwarding (both execution modes) and eval('arguments.length') visibility (interpreter-only)
  • Test262 interpreted baseline: diff clean — the decisive gate, since arguments semantics (sloppy/strict, extras past arity, apply/call interplay) are heavily covered there

…that observe it

Every interpreter call of a user function built a List + SharpTSArray
(+ per-arg ToObject boxing on the V2 path) to Define the JS `arguments`
binding — paid on every call even though almost no function touches it.

Hoist ClosureAnalyzer's ArgumentsRefScanner (which already encodes the
spec scoping: stop at nested non-arrow function boundaries, descend
into true arrows) to Parsing/Visitors as the single shared source, and
gate all four Define sites (SharpTSFunction and the function-expression
paths, boxed + V2) on a per-declaration ArgumentsUsage cache
(ConditionalWeakTable keyed by AST node, so function factories scan
once per declaration, not per closure).

Conservative on direct eval: interpreter eval runs against the live
scope chain, so `eval("arguments[0]")" can observe the binding without
the identifier appearing in the AST — any reference to `eval` in the
body keeps the materialization (scanner option; the compiled closure
analyzer keeps its existing behavior).

Measured (in-process lex+parse+check+interpret loop, call-heavy script,
Release, medians of 5): 175,437 -> 123,882 KB allocated per run
(-29%), gen0 573 -> 404 (-29%), 106.1 -> 94.4 ms (-11%).

Verified: full suite 15452/0; new tests for the apply-forwarding
function-expression chain (both modes) + direct-eval visibility
(interpreter); Test262 interpreted baseline diff clean.
@nickna
nickna merged commit 422281f into main Jul 12, 2026
2 checks passed
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