Skip to content

Improve function call performance#2281

Merged
lahma merged 5 commits into
sebastienros:mainfrom
lahma:function-call-perf
Feb 15, 2026
Merged

Improve function call performance#2281
lahma merged 5 commits into
sebastienros:mainfrom
lahma:function-call-perf

Conversation

@lahma

@lahma lahma commented Feb 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Introduces fixed-slot array-based binding storage for function and block scope environments, replacing HybridDictionary lookups with direct array indexing for qualifying functions and blocks. This significantly reduces allocations and improves throughput for function-heavy workloads.

Approach

Function Environment Optimization

  • Pre-analyzes function definitions at parse time to determine eligibility for fixed-slot storage
  • Qualifying functions: simple parameters, no duplicates, no parameter expressions, no eval, no arguments object usage, no hoisted function initializers, ≤16 total slots
  • Supports both parameters and let/const lexical declarations in the slot array (layout: [params...][vars...][lexical...])
  • Uses Binding[] arrays indexed by parameter/variable position instead of dictionary lookups
  • Implements slot array caching via Interlocked.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 allocations
  • Fast FDI (FunctionDeclarationInstantiation) path in Engine.cs bypasses most of the standard FDI logic for qualifying functions without lexical declarations

Smarter Environment Escape Analysis

  • Instead of conservatively marking any function with closures as having an escaping environment, analyzes whether closures actually reference any slot variable names
  • MayEscapeWithReferences walks the function body, finding closures (function expressions, arrow functions, classes)
  • ClosureReferencesAny recursively scans closure bodies (including nested functions) for identifier matches against slot names
  • Functions with closures that only use globals or their own locals can still benefit from slot caching
  • eval() and with statements still conservatively prevent caching

Block Environment Optimization

  • Block scopes with only let/const declarations (no functions/classes), ≤16 bindings, and no escaping closures (CanReuseEnvironment=true) use fixed-slot arrays
  • Entire DeclarativeEnvironment objects are cached and reused for qualifying blocks via BlockState._cachedEnv
  • When the environment can be reused, Array.Clone() is used instead of creating a new DeclarativeEnvironment

For-Loop Optimization

  • Detects when let-declared loop variables are not captured by any closure inside the loop body
  • Skips the per-iteration CreatePerIterationEnvironment call (which clones the environment each iteration) when safe to do so

Safety & Correctness

  • EnvironmentEscapeAstVisitor detects closures (arrow functions, function expressions, class declarations) and eval() calls that would prevent environment reuse
  • All 10 binding methods in DeclarativeEnvironment have _slots is not null guard checks, falling back to the standard HybridDictionary path for non-qualifying environments
  • Proper TDZ handling: slot path returns true for uninitialized bindings (value=null) to match dictionary behavior — callers handle null values as TDZ errors
  • Test262 conformance: 90,633 pass, 0 fail, 3,462 skipped (identical to main, 26 pre-existing Temporal/ShadowRealm failures)

Benchmark Results

Benchmarks run on .NET 10.0, x64, RyuJIT. Comparison of this branch vs main across Dromaeo, SunSpider, and FunctionCall suites (53 benchmark variants total, 5 warmup + 5 iterations).

Summary (excluding 7 noisy benchmarks with CoV>15%)

Metric Value
Faster (>1.5%) 27 benchmarks
Neutral (±1.5%) 7 benchmarks
Slower (>1.5%) 12 benchmarks
Average speedup +4.0%
Median speedup +5.1%
Average allocation reduction -17.4%

Detailed Results (sorted by speedup)

Benchmark Main (ms) Branch (ms) Speed Alloc
SS:string-validate-input 53.7 41.5 +22.8% -17.9%
SS:string-fasta 143.7 111.1 +22.7% -4.6%
DR:StringBase64(F,T) 39.0 32.2 +17.4% ~0%
DR:ObjectRegExp(T,T) 134.3 111.6 +16.9% -6.6%
DR:ObjectRegExp(F,T) 130.4 108.8 +16.6% +0.4%
SS:string-unpack-code 59.2 50.4 +14.9% -15.8%
SS:3d-raytrace 113.1 96.8 +14.4% -57.7%
FC:NestedCalls_1M 283.5 243.1 +14.3% -49.8%
DR:StringBase64(T,F) 43.6 38.3 +12.3% -81.5%
SS:crypto-md5 67.4 59.3 +12.0% -44.7%
SS:bitops-3bit-byte 92.5 81.7 +11.7% -51.0%
SS:date-format-xparb 40.6 35.9 +11.6% -35.9%
FC:MultiParamCalls_500K 267.0 236.5 +11.4% -54.7%
SS:3d-morph 114.9 102.8 +10.6% ~0%
DR:StringBase64(T,T) 42.7 38.3 +10.3% -82.1%
SS:access-binary-trees 58.3 52.6 +9.7% -20.5%
SS:access-fannkuch 304.1 276.4 +9.1% +0.3%
SS:string-base64 54.4 49.8 +8.4% ~0%
SS:3d-cube 118.7 108.7 +8.4% -6.4%
FC:SimpleCalls_1M 363.6 334.6 +8.0% -43.8%
SS:math-spectral-norm 75.8 70.6 +6.9% -40.4%
DR:CoreEval(F,F) 22.0 20.8 +5.4% +0.3%
SS:control-flow-recursive 72.9 69.0 +5.3% -41.7%
DR:ObjectString(T,T) 172.5 163.8 +5.1% -1.1%
DR:CoreEval(T,F) 20.8 19.8 +4.6% ~0%
SS:string-tagcloud 55.0 52.5 +4.5% -21.9%
DR:CoreEval(T,T) 21.0 20.3 +3.1% ~0%
SS:math-cordic 209.2 203.1 +2.9% -27.3%
DR:ObjectString(T,F) 176.7 175.0 +0.9% -1.1%
SS:math-partial-sums 75.9 75.5 +0.5% ~0%
DR:CoreEval(F,T) 21.4 21.4 +0.4% +0.2%
SS:access-nsieve 75.7 75.7 +0.1% ~0%
DR:Cube(F,T) 30.7 30.7 ~0% -8.0%
SS:access-nbody 117.5 117.8 -0.3% -10.8%
SS:regexp-dna 94.1 94.4 -0.3% ~0%
SS:crypto-sha1 65.1 66.4 -1.9% -39.4%
DR:ObjectString(F,F) 169.0 172.6 -2.1% ~0%
SS:bitops-bits-in-byte 135.8 138.7 -2.1% -55.0%
SS:date-format-tofte 64.7 66.8 -3.2% -4.6%
SS:crypto-aes 75.0 77.9 -3.8% -33.7%
DR:ObjectString(F,T) 168.3 174.9 -3.9% ~0%
DR:Cube(T,F) 29.1 30.5 -4.7% -9.5%
DR:StringBase64(F,F) 31.6 33.5 -5.9% ~0%
SS:bitops-nsieve-bits 132.2 143.6 -8.6% ~0%
SS:bitops-bitwise-and 76.5 85.2 -11.3% ~0%
DR:ObjectArray(T,T) 39.4 44.3 -12.4% -14.1%

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 null guard checks added to all 10 DeclarativeEnvironment binding methods. Since these methods are sealed override, a subclass approach is not feasible — the null check overhead applies to all environments, including GlobalDeclarativeEnvironment and ModuleEnvironment which 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-byte is -2.1% speed but -55.0% allocations).

Changes

Files Modified

  • DeclarativeEnvironment.cs — Added _slots/_slotNames fields, SlotIndexOf method, slot guard checks in all binding methods, TDZ-correct TryGetBinding for uninitialized slots
  • JintEnvironment.cs — Factory method NewFunctionEnvironment creates slot arrays and handles caching
  • FunctionEnvironment.csInitializeParameters fast path for slot-based parameter initialization
  • JintFunctionDefinition.csState class with slot qualification logic (params + vars + let/const), EnvironmentEscapeAstVisitor with smart reference analysis (MayEscapeWithReferences, ScanForCapturingReferences, ClosureReferencesAny), VarSlotCount for lexical offset calculation
  • JintBlockStatement.csBlockState for block slot eligibility, environment caching/reuse (gated on CanReuseEnvironment)
  • JintForStatement.cs — Per-iteration environment skip when loop variables aren't captured
  • Engine.cs — Fast FDI path for qualifying functions, slow path with lexical slot initialization
  • ScriptFunction.cs — Slot array caching in Call method via Interlocked.Exchange

Commits

  1. Improve function call performance — Core fixed-slot implementation
  2. Fix — Minor followup fix
  3. Refine function call performance optimizations — Remove AggressiveInlining from SlotIndexOf, gate block slots on CanReuseEnvironment, add eval() detection, env variable naming cleanup
  4. Expand fixed-slot optimization to functions with let/const declarations — Include lexical declarations in slot array, TDZ support, VarSlotCount for offset calculation
  5. Smarter environment escape analysis for slot caching — Analyze whether closures actually reference slot variables instead of conservatively marking all closures as escaping

@lahma
lahma force-pushed the function-call-perf branch 2 times, most recently from c96c331 to 4f3e212 Compare February 15, 2026 16:56
@lahma
lahma requested a review from Copilot February 15, 2026 17:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Exchange to 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
lahma force-pushed the function-call-perf branch from cdf2174 to 7a34963 Compare February 15, 2026 18:19
lahma and others added 5 commits February 15, 2026 20:53
- 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
lahma force-pushed the function-call-perf branch from 9ec657e to 6ca1205 Compare February 15, 2026 18:53
@lahma
lahma marked this pull request as ready for review February 15, 2026 19:05
@lahma
lahma merged commit 69ba17b into sebastienros:main Feb 15, 2026
4 checks passed
@lahma
lahma deleted the function-call-perf branch February 15, 2026 19:07
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.

2 participants