Make per-call nested function instantiation allocation-free#2684
Merged
Conversation
lahma
enabled auto-merge (squash)
July 14, 2026 07:16
lahma
force-pushed
the
perf-nested-fn-instantiation
branch
from
July 14, 2026 08:09
69c35be to
aa59c18
Compare
Every instantiation of a script function (nested function declarations re-instantiate on each call of their enclosing function) eagerly allocated a JsString for the name, a name PropertyDescriptor, a length LazyPropertyDescriptor and, via MakeConstructor, a prototype LazyPropertyDescriptor. Descriptors cannot be shared across instances (DefineOwnProperty mutates them in place), so instead a shared pending sentinel now marks each property as existing-but-not-materialized and GetOwnProperty swaps in the real descriptor on first read. The name JsString is cached once per JintFunctionDefinition. Behavior (descriptor shapes, key order, delete/redefine semantics) is unchanged; the sentinel's value accessors throw so an unmaterialized leak fails loudly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A non-strict, non-arrow, non-generator, non-async ScriptFunction eagerly allocated a ThrowerPropertyDescriptor, a caller PropertyDescriptor and the property dictionary holding them on every instantiation. The two restricted properties now live in dedicated ScriptFunction fields (the same mechanism as name/length/prototype) initialized to the shared pending sentinel and materialized on first read, so a plain sloppy function instantiates with no dictionary and no descriptor allocations at all. A deleted-then-redefined restricted property demotes to the dictionary, preserving the previous end-of-key-order resurrection semantics; the initial enumeration order (length, name, prototype, arguments, caller) is unchanged. MakeMethod now removes the pair via the cached JsStrings instead of allocating fresh ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FunctionDeclarationInstantiation re-runs its function-hoisting loop on every call of a declaration-bearing function. The hoisted declarations now live in a plain array of (declaration, Key) pairs on the shared State instead of a LinkedList, so the per-call loop stops chasing list nodes and stops re-hashing each function name into a Key; the Function intrinsic is resolved once per call instead of per declaration. InstantiateFunctionObject also skips the SetFunctionName call for named declarations — the function's (pending) own name was already established at construction, so the call was a guaranteed early-return that still allocated a JsString for the name argument on every instantiation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The no-fixed-slots var-initialization loop in FunctionDeclarationInstantiation inserted bindings one CreateMutableBindingAndInitialize at a time, cutting the hybrid dictionary's list over to hash storage mid-loop on every call of a declaration-heavy function (such environments escape and are rebuilt per call). EnsureCapacity once up front, mirroring the lexical-declarations path below. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The newTarget != this arm resolves the prototype through the newTarget instead, so a pending descriptor can stay pending there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lahma
force-pushed
the
perf-nested-fn-instantiation
branch
from
July 14, 2026 08:24
aa59c18 to
7567550
Compare
This was referenced Jul 15, 2026
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.
Summary
Every call of a function whose body declares inner functions re-instantiates those functions (spec-required — each call's closures capture that call's environment). ETW profiling shows the instantiation chain (
FunctionDeclarationInstantiation→InstantiateFunctionObject→ScriptFunction..ctor) at 4.6% of program time on closure-producing workloads. This makes each instantiation cheaper — instantiating a named sloppy nested function drops from ~7 side allocations (~250–300 B) to zero (only theScriptFunctionobject itself remains, +16 B for two new lazy fields).Five commits:
PropertyDescriptoron first read (sharing actual descriptors across instances is provably unsafe:ValidateAndApplyPropertyDescriptormutates them in place). The sentinel's value accessors throw so a leak fails loudly. The per-definition-constant function nameJsStringis cached onJintFunctionDefinition.arguments/callerpoison descriptors — a plain sloppy function no longer creates any property dictionary at all; delete-then-redefine demotes to dictionary preserving key-resurrection order; initial key order unchanged; %ThrowTypeError% resolution stays deferred exactly as before.FunctionsToInitializebecomes a readonly record-struct array (wasLinkedList) with the bindingKeyprecomputed at BuildState; the intrinsic lookup is hoisted out of the loop;SetFunctionName's per-callJsStringallocation is skipped for named declarations.[[Construct]]only when consulted.Functional verification beyond suites: 77 checks (descriptor shapes before/after calls via
getOwnPropertyDescriptor, key order, delete/redefine/freeze, loop-closures, NFE recursion, generators/async, Proxy, bind,with, class) — output byte-identical to a pristine main build.Benchmarks (default jobs, adjacent A/B on latest main)
Gates
🤖 Generated with Claude Code