Trim per-iteration block ceremony; slot lane for lexical declaration initialization#2583
Merged
Merged
Conversation
…l slot lane Three per-iteration cost cuts for lexical blocks in hot loops (stopwatch-modern 227.0 -> 190.6 ms/iter locally, -16%; classic stopwatch unchanged): - JintBlockStatement.ExecuteBlock: when the block environment registered no dispose resources, finalize inline instead of round-tripping a DisposeStepResult through CompleteDispose (BeginDispose on an empty capability is a pure Done). - ExecutionContextStack.ReplaceTopLexical/VariableEnvironment: write the single field in place instead of rebuilding the 10-field ExecutionContext struct; the instance lives in a mutable array slot. - JintVariableDeclaration: slot lane for let/const identifier declarations — resolve the slot index once per slot-layout identity (Key[] identity is stable across engines for shared prepared scripts) and initialize the slot directly, skipping the pooled Reference round-trip and name scan. Function-definition/class initializers, using declarations, destructuring, and eval/arguments names stay on the existing path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The inline lexical arm grew ExecuteInternal enough to cost var-only call workloads ~6% through pure code-size/layout effects (MixedArithmetic 15.38 -> 16.28 ms). Keeping the slot lane inline and extracting only the Reference-based slow arm restores the var path (15.61 ms) without giving back the lane win. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 13, 2026
This was referenced Jul 20, 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.
Modern
let/constloop bodies pay a large per-iteration tax thatvar-based code doesn't: in the engine-comparison table,stopwatch-modernruns ~55 ms/iter slower than classicstopwatchon identical logic. Probe attribution (A/B script variants viaCpuProfileDriver) decomposed that gap into block-environment ceremony (~42%), lexical const-init overhead (~20%), let-loop header residual (~22%), and nested global reads (~13%). This PR removes the first two components with three targeted changes:JintBlockStatement.ExecuteBlock: when the block environment registered no dispose resources (nousing/await usingdeclaration ran), finalize inline — restore the outer environment and park the reusable env — instead of round-tripping aDisposeStepResultthroughCompleteDispose.BeginDisposeon an empty capability is a pureDone(c), so semantics are unchanged.ExecutionContextStack.ReplaceTopLexicalEnvironment/ReplaceTopVariableEnvironment: write the single field in place (Unsafe.AsRef) instead of constructing and copying back the whole 10-fieldExecutionContextstruct. The instance lives in a mutable array slot, so casting away the field'sreadonlyis safe; every block entry/exit and function-call env switch benefits.JintVariableDeclaration: a slot lane forlet/constidentifier declarations. The declaration's own binding lives in the current environment by construction (blocks/loops instantiate their environment before executing statements), so when that environment stores bindings in fixed slots the initializer value is written straight into the resolved slot. The slot index is cached per slot-layout identity (Key[]identity is stable across engines for shared prepared scripts; publication is a single reference write, so shared handler trees stay race-free). This skips the pooledReferencerent/evaluate/return round-trip and theSlotIndexOfscan. Excluded statically and left on the existing path:var,using/await using, destructuring targets,eval/argumentsnames, and function-definition/class-expression initializers (those need the reference's name forSetFunctionName/EvaluateWithName). The Reference-based arm moved to a non-inlined method so the var-declaration path stays compact (an earlier inline version costvar-only workloads ~6% through pure code-size effects).Benchmarks
BenchmarkDotNet default jobs, win-x64, .NET 10, A/B against upstream/main from separate worktrees (each run from its own CWD).
StopwatchBenchmark (the target):
GeneratorBenchmark (block-heavy bodies benefit):
No-regression gates: ClosureCallBenchmarks (EmptyClosureCall −8.2%, CapturedVarReadWrite +1.3%, ManyLocalCall −1.5%; ParamLocalCall statistically identical across 5-launch runs: 104.2 ± 5.7 → 103.2 ± 4.1 ms), FunctionLocalNumberLoopBenchmark (−1.2%/−2.8%/+1.1%/+1.4%), DromaeoBenchmark Cube modern −4..−12%, Cube classic within its observed run-to-run band. Dromaeo CoreEval turned out to be bimodal on identical bits (baseline itself swung 2.9 → 9.6 ms between runs) and cannot gate either way.
Allocations unchanged on every suite.
Verification
Jint.Testsgreen (net10.0 + net472),Jint.Tests.PublicInterfacegreen🤖 Generated with Claude Code