perf(interp): lazy arguments materialization (−29% alloc on call-heavy code)#1276
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Round 9b: the top remaining interpreter perf item from the July audits.
Problem
Every interpreter call of a user function unconditionally materialized the JS
argumentsbinding — aList<object?>+SharpTSArray, plus per-argToObject()boxing on the CallV2 span path — even though the overwhelming majority of functions never reference it.Change
ClosureAnalyzer's privateArgumentsRefScannertoParsing/Visitors/ArgumentsRefScanner.csas the single shared source. It already encodes the spec scoping rules: stop at nested non-arrow function boundaries (they bind their ownarguments), descend into true arrows (they inherit lexically). The compiled-mode analyzer keeps identical behavior.ArgumentsUsagecache (ConditionalWeakTablekeyed by declaration AST node — function factories scan once per declaration, not per closure) gates all fourDefine("arguments", …)sites:SharpTSFunction.Call/CallV2and the function-expression paths.evalruns against the live scope chain, soeval("arguments[0]")can observe the binding without the identifier appearing in the AST. The scanner takes atreatEvalReferenceAsUseoption (true for the interpreter gate, false for the compiled analyzer's existing behavior) — anyevalreference 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:
Verification
function(){ return func.apply(this, arguments); }forwarding (both execution modes) andeval('arguments.length')visibility (interpreter-only)argumentssemantics (sloppy/strict, extras past arity, apply/call interplay) are heavily covered there