Improve function call performance#2281
Merged
Merged
Conversation
lahma
force-pushed
the
function-call-perf
branch
2 times, most recently
from
February 15, 2026 16:56
c96c331 to
4f3e212
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This pull request introduces significant performance optimizations for function calls and block scopes through fixed-slot array-based binding storage. The changes replace dictionary lookups with direct array indexing for qualifying functions and blocks, reducing allocations and improving throughput for function-heavy JavaScript workloads.
Changes:
- Implements fixed-slot array optimization for function and block scope environments with ≤16 bindings, using pre-analyzed eligibility criteria at parse time
- Adds environment and slot array caching via
Interlocked.Exchangeto eliminate allocations when environments don't escape (no closures capture them) - Introduces fast FDI (FunctionDeclarationInstantiation) path for simple functions and per-iteration environment reuse optimization for for-loops with non-captured let variables
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
JintForStatement.cs |
Adds optimization to skip per-iteration environment creation when loop variables aren't captured by closures, using ForLoopMayCapture analysis |
JintBlockStatement.cs |
Implements block scope slot optimization with environment caching/reuse via BlockState, checking eligibility based on declaration types and closure presence |
JintFunctionDefinition.cs |
Adds State fields for slot qualification, EnvironmentEscapeAstVisitor for closure/eval detection, and slot eligibility logic for simple functions |
JintEnvironment.cs |
Modifies NewFunctionEnvironment to allocate and cache slot arrays for qualifying functions |
FunctionEnvironment.cs |
Adds fast path in InitializeParameters for slot-based parameter initialization |
DeclarativeEnvironment.cs |
Adds _slots/_slotNames fields and slot-based implementations of all 10 binding methods with dictionary fallback |
ScriptFunction.cs |
Implements slot array caching in Call method via Interlocked.Exchange for non-escaping environments |
Engine.cs |
Adds fast FDI path in FunctionDeclarationInstantiation for simple functions with slot storage |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
lahma
force-pushed
the
function-call-perf
branch
from
February 15, 2026 18:19
cdf2174 to
7a34963
Compare
- Remove AggressiveInlining from SlotIndexOf to prevent code bloat - Gate block slots on CanReuseEnvironment to avoid overhead for blocks with closures - Add eval() detection to EnvironmentEscapeAstVisitor for correctness - Cache state local in ScriptFunction.Call for safe Interlocked.Exchange - Clean up variable naming in Engine.FunctionDeclarationInstantiation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Functions with top-level let/const variable declarations now qualify for fixed-slot array storage. Lexical bindings are added to the slot array after parameters and vars, with proper const/let initialization semantics. Key changes: - BuildState: relaxed qualification to allow LexicalDeclarations when all are lexical-scoped (no function/class declarations) - Added VarSlotCount field to State for correct lexical slot offset - Engine FDI: lexical declarations use slots when available - TryGetBinding: slot path now returns true for uninitialized bindings (matching dictionary behavior) to preserve temporal dead zone errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of conservatively marking any function with closures as having escaping environments, analyze whether the closures actually reference any of the function's slot variables. This allows slot array caching for functions that contain closures which only use globals or their own local variables. - Add MayEscapeWithReferences that checks closure bodies for slot name references - ScanForCapturingReferences walks the function body, finding closures - ClosureReferencesAny recursively checks closure bodies including nested functions - eval() and with statements still conservatively mark as escaping - Add 5 tests covering: non-referencing closures, referencing closures, nested transitive references, globals-only closures, slot caching reuse Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
lahma
force-pushed
the
function-call-perf
branch
from
February 15, 2026 18:53
9ec657e to
6ca1205
Compare
lahma
marked this pull request as ready for review
February 15, 2026 19:05
This was referenced Feb 16, 2026
This was referenced Mar 1, 2026
This was referenced Jun 8, 2026
This was referenced Jun 29, 2026
This was referenced Jul 7, 2026
This was referenced Jul 13, 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
Introduces fixed-slot array-based binding storage for function and block scope environments, replacing
HybridDictionarylookups with direct array indexing for qualifying functions and blocks. This significantly reduces allocations and improves throughput for function-heavy workloads.Approach
Function Environment Optimization
eval, noargumentsobject usage, no hoisted function initializers, ≤16 total slotslet/constlexical declarations in the slot array (layout:[params...][vars...][lexical...])Binding[]arrays indexed by parameter/variable position instead of dictionary lookupsInterlocked.Exchange— when a function's environment doesn't escape (no closures capture slot variables), the array is recycled for the next call to the same function, eliminating repeated allocationsEngine.csbypasses most of the standard FDI logic for qualifying functions without lexical declarationsSmarter Environment Escape Analysis
MayEscapeWithReferenceswalks the function body, finding closures (function expressions, arrow functions, classes)ClosureReferencesAnyrecursively scans closure bodies (including nested functions) for identifier matches against slot nameseval()andwithstatements still conservatively prevent cachingBlock Environment Optimization
let/constdeclarations (no functions/classes), ≤16 bindings, and no escaping closures (CanReuseEnvironment=true) use fixed-slot arraysDeclarativeEnvironmentobjects are cached and reused for qualifying blocks viaBlockState._cachedEnvArray.Clone()is used instead of creating a newDeclarativeEnvironmentFor-Loop Optimization
let-declared loop variables are not captured by any closure inside the loop bodyCreatePerIterationEnvironmentcall (which clones the environment each iteration) when safe to do soSafety & Correctness
EnvironmentEscapeAstVisitordetects closures (arrow functions, function expressions, class declarations) andeval()calls that would prevent environment reuseDeclarativeEnvironmenthave_slots is not nullguard checks, falling back to the standardHybridDictionarypath for non-qualifying environmentstruefor uninitialized bindings (value=null) to match dictionary behavior — callers handle null values as TDZ errorsBenchmark Results
Benchmarks run on .NET 10.0, x64, RyuJIT. Comparison of this branch vs
mainacross Dromaeo, SunSpider, and FunctionCall suites (53 benchmark variants total, 5 warmup + 5 iterations).Summary (excluding 7 noisy benchmarks with CoV>15%)
Detailed Results (sorted by speedup)
7 benchmarks with high variance (CoV>15%) excluded: Cube(F,F), Cube(T,T), ObjectArray(F,F), ObjectArray(F,T), ObjectArray(T,F), ObjectRegExp(F,F), ObjectRegExp(T,F). These showed GC-induced outliers with StdDev up to 48ms on 78ms means.
Notes on Regressions
The small regressions (2-12% range) are inherent to the
_slots is not nullguard checks added to all 10DeclarativeEnvironmentbinding methods. Since these methods aresealed override, a subclass approach is not feasible — the null check overhead applies to all environments, includingGlobalDeclarativeEnvironmentandModuleEnvironmentwhich never use slots. This is the fundamental trade-off of this approach.Benchmarks that don't exercise function calls heavily (pure math, bitops, regexp) see marginal overhead from the null checks without benefiting from slot optimization. Even many of the "slower" benchmarks still show significant allocation reductions (e.g.,
bitops-bits-in-byteis -2.1% speed but -55.0% allocations).Changes
Files Modified
DeclarativeEnvironment.cs— Added_slots/_slotNamesfields,SlotIndexOfmethod, slot guard checks in all binding methods, TDZ-correctTryGetBindingfor uninitialized slotsJintEnvironment.cs— Factory methodNewFunctionEnvironmentcreates slot arrays and handles cachingFunctionEnvironment.cs—InitializeParametersfast path for slot-based parameter initializationJintFunctionDefinition.cs—Stateclass with slot qualification logic (params + vars + let/const),EnvironmentEscapeAstVisitorwith smart reference analysis (MayEscapeWithReferences,ScanForCapturingReferences,ClosureReferencesAny),VarSlotCountfor lexical offset calculationJintBlockStatement.cs—BlockStatefor block slot eligibility, environment caching/reuse (gated onCanReuseEnvironment)JintForStatement.cs— Per-iteration environment skip when loop variables aren't capturedEngine.cs— Fast FDI path for qualifying functions, slow path with lexical slot initializationScriptFunction.cs— Slot array caching inCallmethod viaInterlocked.ExchangeCommits