Skip to content

Fused modulo-equality lane; admit const bindings to comparison-lane reads#2604

Merged
lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf-fused-modulo-test
Jul 9, 2026
Merged

Fused modulo-equality lane; admit const bindings to comparison-lane reads#2604
lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf-fused-modulo-test

Conversation

@lahma

@lahma lahma commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto latest main after #2603 merged.

What

Two commits, each independently measured:

  1. Fused modulo-equality lane: identifier % numericConstant ==/===/!=/!== numericConstant
    the stopwatch.js if-chain shape (z % 2 == 0, executed ~820k times per benchmark op) — is
    evaluated as one fused lane on the equality expressions: the identifier is read through the same
    slot/global arms as the comparison lane and the whole test runs on raw doubles, skipping the
    modulo node's dispatch, operand materialization and result boxing.
  2. Admit immutable bindings to lane reads: TryGetNumberSlot requires binding.Mutable so its
    read-modify-write callers can pair it with SetNumberSlot unchecked — but the comparison lanes
    reused it for pure reads, inheriting the write-side constraint. Consequence: no lane ever fired
    on a const operand
    (stopwatch-modern's entire const z chain declined per evaluation). The
    lanes now read through TryGetNumberSlotForRead, a read-only sibling without the gate.

Semantics

C# double % implements Number::remainder exactly; the only integer-vs-double remainder
divergence (−0 vs +0 results) is erased by the equality consumer (IEEE == treats them equal) — so
the fused result is spec-exact for any proven-Number operand: NaN dividends, % 0 and NaN constants
compare false, v % ±Infinity == v. TDZ stays intact for the const admission: an uninitialized
binding has neither an unboxed number nor a reference value, so it declines and the generic path
raises the ReferenceError. Writers keep the Mutable-gated form (compound assignment on a const still
throws through the generic path).

Results

Commit 1 (fused lane), A/B vs the global-arm base (default job, adjacent runs):

row before after
LoopDispatch.ModuloEqualTest 8.05 ms / 2,808 KB 5.56 ms / 3.3 KB −31% time, −99.9% alloc
stopwatch driver 167.9 ms/iter 152.6 ms/iter −9.1%
StopwatchBenchmark classic Execute 150.0 ms 136.5 ms −9.0%
StopwatchBenchmark classic Prepared 158.4 ms 141.8 ms −10.5%
stopwatch-modern driver 183.9 ms/iter 187.5 ms/iter +2.0% — the const gap, fixed by commit 2

Commit 2 (const admission), cumulative A/B vs the same base:

row before after
StopwatchBenchmark classic Execute 158.6 ms 141.6 ms −10.7%
StopwatchBenchmark classic Prepared 152.9 ms 137.5 ms −10.1%
StopwatchBenchmark modern Execute 173.7 ms 153.0 ms −11.9%
StopwatchBenchmark modern Prepared 169.7 ms 149.8 ms −11.7%
stopwatch-modern driver 186.3 ms/iter 167.7 ms/iter −10.0% (was +2.0% before commit 2)
stopwatch classic driver 167.1 ms/iter 153.0 ms/iter −8.4%

Allocations flat on every row.

Tests

Fused-lane sign/zero behavior across negative dividends, NaN / % 0 / Infinity specials, the
coercing decline, the global-binding shape, const bindings through all three lane forms, and the TDZ
ReferenceError.

Gating

All-TFM build ✅ · Jint.Tests ×2 ✅ · PublicInterface ×2 ✅ · Test262 99,260/0 ✅

🤖 Generated with Claude Code

@lahma
lahma force-pushed the perf-fused-modulo-test branch from 5adb9f8 to c631b77 Compare July 9, 2026 07:27
@lahma
lahma enabled auto-merge (squash) July 9, 2026 07:59
@lahma
lahma force-pushed the perf-fused-modulo-test branch from c631b77 to 6134981 Compare July 9, 2026 08:11
lahma and others added 2 commits July 9, 2026 11:48
`identifier % numericConstant ==/===/!=/!== numericConstant` - the
stopwatch.js if-chain shape, executed ~820k times per benchmark op - is
now evaluated as one fused lane on the equality expressions: the
identifier is read through the same slot/global arms as the comparison
lane and the whole test runs on raw doubles, skipping the modulo node's
dispatch, operand materialization and result boxing.

C# double % implements Number::remainder exactly, and the only
integer-vs-double remainder divergence (-0 vs +0 results) is erased by
the equality consumer (IEEE == treats them equal), so the fused result
is spec-exact for any proven-Number operand: NaN dividends, % 0 and
NaN constants compare false (true negated), v % ±Infinity == v.
Non-number operands decline to the generic coercing path pre-effect.

A/B (default job, adjacent runs, vs the global-arm base):

- LoopDispatchBenchmarks.ModuloEqualTest 8.05 -> 5.56 ms (-31%),
  allocation 2,808 KB -> 3.3 KB (Gen0 eliminated)
- stopwatch driver 167.9 -> 152.6 ms/iter (-9.1%); StopwatchBenchmark
  classic Execute -9.0%, classic prepared -10.5%, allocations flat

Tests pin sign/zero behavior across negative dividends, NaN / % 0 /
Infinity specials, the coercing decline, and the global-binding shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TryGetNumberSlot requires binding.Mutable so that its read-modify-write
callers may pair it with SetNumberSlot unchecked - but the comparison,
equality and fused-modulo lanes reused it for pure reads, inheriting
the write-side constraint. Consequence: no lane ever fired on a `const`
operand, and stopwatch-modern's entire `const z` if-chain (plus every
`const` bound/limit in loop tests anywhere) declined per evaluation -
turning the fused-modulo commit into a small regression on that row
(driver +2.0%).

The lanes now read through TryGetNumberSlotForRead, a read-only sibling
without the Mutable gate. TDZ stays intact: an uninitialized binding
has neither an unboxed number nor a reference value, so it declines and
the generic path raises the ReferenceError. Writers keep the
Mutable-gated form (compound assignment on a const must still throw
through the generic path).

A/B vs the global-arm base, both fused-modulo commits applied (default
job, adjacent runs):

| StopwatchBenchmark   | base     | fused+const |
|----------------------|----------|-------------|
| classic Execute      | 158.6 ms | 141.6 ms (-10.7%) |
| classic Prepared     | 152.9 ms | 137.5 ms (-10.1%) |
| modern Execute       | 173.7 ms | 153.0 ms (-11.9%) |
| modern Prepared      | 169.7 ms | 149.8 ms (-11.7%) |

Drivers: stopwatch-modern 186.3 -> 167.7 ms/iter (-10.0%, was +2.0%
before this commit), classic 167.1 -> 153.0 (-8.4%). Allocations flat.

Tests pin the const shapes through all three lane forms and the TDZ
ReferenceError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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