Share compiled statement lists across generator and async invocations#2571
Merged
Conversation
JintStatementList._index made the compiled statement handler tree mutable, forcing generators (sebastienros#2567) to rebuild the whole tree per invocation to keep resume positions from being shared across live instances. Positions now live in the suspendable's SuspendDataDictionary — completing the pattern already used for all other suspension state — so the handler tree is immutable and generator/async generator bodies can share the cached list again. Sync execution reads the frame suspendable once per list execution (it was already read per statement for the suspension checks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Async statement bodies allocated a fresh JintStatementList (rebuilding the whole compiled handler tree) on every invocation to keep resume positions per instance. With positions stored on the AsyncFunctionInstance, the cached list is safe to share, so async calls stop paying a per-call tree rebuild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Small generators regressed ~11% from paying dictionary hash operations on every yield/resume for their single body list. Most suspendables only ever track one list, so a one-entry slot serves the common case with reference comparisons and field access; nested-list suspensions fall back to the dictionary. A key never moves between slot and dictionary, so lookups cannot observe stale entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lahma
enabled auto-merge (squash)
July 4, 2026 09:16
This was referenced Jul 6, 2026
This was referenced Jul 7, 2026
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.
Follow-up to #2567. That fix gave each generator instance its own
JintStatementListto stop live instances from corrupting each other's resume position — at the cost of rebuilding the whole compiled statement handler tree on every generator instantiation (a price async statement bodies had also been paying per invocation since their introduction).Change
JintStatementListis now immutable after construction: the mutable resume position (_index) moves into the suspendable instance'sSuspendDataDictionary, keyed by the list — completing the pattern already used for every other piece of suspension state (loop iterators, destructuring, block environments). With positions per instance, the cached list is safe to share, so:_bodyStatementList ??=),Most suspendables only ever track one list (the function body), so a one-entry inline slot fronts the position dictionary; nested-list suspensions fall back to the dictionary. A key never moves between slot and dictionary, so lookups cannot observe stale entries. The sync execution path only gains one frame-suspendable read per list execution — it was already read per statement for the suspension checks.
Benchmarks
BenchmarkDotNet default job, .NET 10, A/B against
main(059f174) from separate worktrees, idle machine.GeneratorBenchmarkis added in this PR (small bodies, a large body with nested blocks/loops/switch, and interleaved instances).Sync neutrality probes (StopwatchBenchmark ×4, RecursionBenchmark Fib/Tak/DeepSum): allocations byte-identical to main on all seven; time deltas ranged −4.9%..+6.4% with inconsistent signs across closely related methods (e.g. Stopwatch Execute +5.3% while Execute_ParsedScript −2.8%), i.e. run-to-run noise rather than a systematic shift.
Verification
🤖 Generated with Claude Code