Skip to content

Extend for-loop tight-body lane to if/else chains and variable declarations#2623

Merged
lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf-tight-if-body
Jul 10, 2026
Merged

Extend for-loop tight-body lane to if/else chains and variable declarations#2623
lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf-tight-if-body

Conversation

@lahma

@lahma lahma commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Extends the for-loop tight-body lane (#2605) beyond expression-statement-only bodies to the shape real hot loops actually have — the stopwatch benchmark's body being the canonical case: a var/const declaration, a 4-way else if modulo chain whose branches call tiny closures, and trailing member-read declarations, executed 391k times per benchmark op with full per-statement ceremony (PrepareFor, Completion materialization, statement-list bookkeeping, nested-if suspension probes).

Shape predicate. The body may now contain expression statements, var/let/const declarations (using/await using excluded — dispose tracking), and if/else chains over such statements, including declaration-free brace blocks. Anything that routes completions — break/continue/return/labels/loops/switch/try — and AnnexB function-declaration branches still disqualify structurally, so every admitted completion is Normal by construction and the lane needs no completion routing.

Dispatch. A new virtual JintStatement.ExecuteDiscarded (default: full Execute) with lean overrides:

  • JintIfStatement: unboxed GetBooleanValue test → recurse into the branch; drops PrepareFor, suspension-range checks, UpdateEmpty and the Completion struct; polls the deferred-error slot after the test so a signalled error suppresses the branch.
  • JintVariableDeclaration: tracks the node for error locations, then runs the existing slot-lane/cached-global initialization (all its paths already end in Empty completions). Skipping PrepareFor is safe: EvaluationContext.Completion is written nowhere else (verified by search), and a stale Target is only consumed under Break/Continue completions, which the shape excludes.
  • Declaration-free nested blocks iterate their statements with the established per-statement Engine._error poll (a statement after a deferred error must not run).

Lexical bodies. Bodies with let/const engage only when #2610 flattening is active: the pooled loop env carries the body slots and the tight loop re-establishes their TDZ per iteration exactly like the generic flattened arm; a lexical body that failed to flatten falls back to the generic path.

Rider. JintConditionalExpression now takes the unboxed GetBooleanValue path for its test in non-suspendable frames (was: materialize JsValue → TypeConverter.ToBoolean).

Nine new pins in TightLoopTests (tight-vs-generic parity for the full if-chain shape, throw-in-branch state/location, statement-after-deferred-error, break/continue/return exclusion semantics, per-iteration TDZ in flattened+tight bodies including iteration ≥2, using dispose ordering, MaxStatements enforcement) plus two new LoopDispatch rows (IfChainLoop = the full stopwatch body shape, VarDeclBody).

Benchmarks (before/after, same machine, sequential; allocations byte-identical on every row)

The newly-eligible if-test rows — the lane firing is the delta:

LoopDispatch row main PR Δ
ComparisonOnly 5.458 ms 2.175 ms −60.2%
StrictEqualTest 4.616 ms 2.107 ms −54.4%
LooseEqualTest 4.511 ms 1.957 ms −56.6%
ModuloEqualTest 6.278 ms 2.305 ms −63.3%
IfChainLoop (new; dominated by its closure calls) 33.20 ms 27.46 ms −17.3%
VarDeclBody (new) 7.917 ms 6.592 ms −16.7%

The target rows:

Row main PR Δ
Stopwatch Execute (classic) 138.0 ms 119.3 ms −13.6%
Stopwatch Execute_ParsedScript (classic) 139.2 ms 112.7 ms −19.0%
Stopwatch Execute (modern) 151.4 ms 132.6 ms −12.4%
Stopwatch Execute_ParsedScript (modern) 148.5 ms 124.2 ms −16.4%
PreparedAnomaly SourcePerOp / Shared / PerOp / ReusedEngine 13.81 / 13.60 / 13.74 / 15.02 ms 11.04 / 11.24 / 10.89 / 12.86 ms −14..−20%

Canary rows not touched by this change moved within the documented single-launch process-mode bands, in both directions (EmptyLoop −5.4%, VariableBoundLoop +17.5% on an empty-body loop the change cannot affect, CounterAdd/LocalCopy/StringAppend +7..11%) with byte-identical allocations; launchCount-5 medians available on request if any row needs adjudication.

Gates: Jint.Tests 3,297+3,234 pass (9 new pins), PublicInterface 82×2*, Test262 99,426 pass / 0 fail (exact baseline).

* the net472 TimeSystemTests.CanUseTimeProvider wall-clock-tolerance test currently fails on this machine on unmodified main as well (verified 3/3, ~7 ms past its 100 ms window, post-sleep timer state) — environmental, not attributable to the change; the net10 arm and all other net472 tests pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv

…ations

The tight loop previously admitted only expression-statement bodies, so
the stopwatch-shaped body (var decl + 4-way else-if chain calling tiny
closures + two member-read var decls) paid full per-statement ceremony:
PrepareFor, Completion materialization, statement-list bookkeeping and
the nested if suspension probes, 391k times per benchmark op.

The body shape predicate now admits expression statements, var/let/const
declarations (using/await-using excluded) and if/else chains over such
statements, including declaration-free brace blocks. Control flow that
routes completions (break/continue/return/labels/loops/switch/try),
other declarations and AnnexB function-declaration branches still
disqualify structurally, so every admitted completion is Normal by
construction and the lane needs no completion routing.

Dispatch happens through a new virtual JintStatement.ExecuteDiscarded
(default: full Execute) with lean overrides for expression statements,
if statements (unboxed GetBooleanValue test, no suspension-range checks,
no UpdateEmpty), variable declarations (slot lane / cached-global route
already end in Empty completions) and declaration-free nested blocks.
Deferred errors keep the existing per-statement Engine._error poll
protocol; statements after a deferred error do not run.

Bodies with lexical declarations engage only when sebastienros#2610 flattening is
active: the pooled loop env carries the body slots and the tight loop
re-establishes their TDZ per iteration exactly like the generic
flattened arm. Rider: JintConditionalExpression now takes the unboxed
GetBooleanValue path for its test in non-suspendable frames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv
@lahma
lahma enabled auto-merge (squash) July 10, 2026 18:22
@lahma
lahma merged commit a28fa10 into sebastienros:main Jul 10, 2026
4 checks passed
@lahma
lahma deleted the perf-tight-if-body branch July 10, 2026 18:36
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