Skip to content

Build the yield argument handler once, not per evaluation - #2593

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf-yield-argument-handler
Jul 6, 2026
Merged

lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf-yield-argument-handler

Conversation

@lahma

@lahma lahma commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

\JintYieldExpression\ called \JintExpression.Build(expression.Argument)\ inside \EvaluateInternal\ — on every yield evaluation. \Build\ only returns a cached handler when the AST node's \UserData\ was pre-populated (prepared scripts), so for ordinary execution every \yield \ reconstructed the argument's whole handler subtree per iteration: for \yield { a: i, b: i + 1 }\ that is a \JintObjectExpression\ + \ObjectProperty[]\ + \ExpressionCache\ + identifier/binary handlers — every time the generator produced a value — and the handler lost all its per-node inline caches on each rebuild. The allocation census flagged this as ~51% of a generator-producing-literals workload (handler types should never appear in steady-state allocation profiles at all).

The argument handler is now built once in the constructor, like every other expression handler.

Measurements

Generator yielding object literals (100k yields, fresh engine per iteration, A/B/A/B alternating binaries): 132.2 → 52.9 MB/iter (−60%), 98.4 → 67.9 ms/iter (−31%). All AST-handler types disappear from the allocation profile; what remains is the literal's dictionary-path descriptors and the user-visible generator result objects.

Verification

  • \Jint.Tests\ green (net10.0 + net472), \Jint.Tests.PublicInterface\ green
  • Full test262: 99,260 passed, 0 failed (generator/async-generator suites cover yield, yield*, resume-with-throw/return and yields in loops)

🤖 Generated with Claude Code

JintYieldExpression called JintExpression.Build(expression.Argument) inside
EvaluateInternal — on every single yield evaluation. Build only returns a
cached handler when the AST node''s UserData was pre-populated (prepared
scripts), so for ordinary execution every `yield <expr>` reconstructed the
argument''s whole handler subtree per iteration: for `yield { a: i, b: i+1 }`
that is a JintObjectExpression + ObjectProperty[] + ExpressionCache +
JintIdentifierExpression and JintBinaryExpression handlers — every time the
generator produced a value. The handler also lost all its per-node inline
caches each rebuild.

The argument handler is now built once in the constructor like every other
expression handler.

Generator-producing-object-literals workload (fresh engine per iteration):
132.2 -> 52.9 MB/iter (-60%), 98.4 -> 67.9 ms/iter (-31%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lahma
lahma enabled auto-merge (squash) July 6, 2026 20:52
@lahma
lahma merged commit 523867d into sebastienros:main Jul 6, 2026
4 checks passed
@lahma
lahma deleted the perf-yield-argument-handler branch July 13, 2026 06:52
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.

1 participant