Skip to content

Add unboxed slot-number lane to equality operators#2602

Merged
lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf-equality-lanes
Jul 9, 2026
Merged

Add unboxed slot-number lane to equality operators#2602
lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf-equality-lanes

Conversation

@lahma

@lahma lahma commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

What

===, !==, == and != now take the same per-node unboxed numeric lane the relational operators
gained in #2574/#2578: when both operands resolve to slot-stored numbers (or a numeric constant), the
comparison runs on raw doubles without materializing either side.

Why

A loop test like i === 50000 re-boxed the counter into a JsNumber on every iteration once it
exceeded the small-int cache — the equality operators were the only comparison family without the lane.

Semantics

Both operands are runtime-proven Numbers (TryGetNumberSlot only succeeds for number-holding
bindings), so the IEEE compare is spec-exact for all four operators: IsLooselyEqual defers to
IsStrictlyEqual for same-type operands, NaN == NaN is false, +0 == -0 is true. BigInt is
excluded by the JsNumber gates; any non-number operand declines to the generic coercing path before
any side effect.

Results

LoopDispatchBenchmarks A/B (default job, adjacent runs; new rows added by this PR):

row base lanes
StrictEqualTest 5.967 ms / 2,808 KB 3.710 ms / 3.26 KB −37.8% time, −99.9% alloc
LooseEqualTest 5.523 ms / 2,808 KB 3.777 ms / 3.27 KB −31.6% time, −99.9% alloc

Gen0 eliminated on both rows; untouched rows byte-identical allocation, time deltas sign-mixed and
resolved as window drift via an A/B/A on identical bits.

ModuloEqualTest (the stopwatch.js if-chain shape) is added as a baseline row but deliberately not
covered here — its left side is a modulo expression, not an identifier (see the follow-up PR).

Tests

EqualityLaneTests pin NaN/signed-zero semantics, mid-loop type-change decline, BigInt/mixed-type
bail, and value positions.

Gating

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

🤖 Generated with Claude Code

===, !==, == and != now take the same per-node numeric lane the
relational operators gained in sebastienros#2574/sebastienros#2578: when both operands resolve
to slot-stored numbers (or a numeric constant), the comparison runs on
raw doubles without materializing either side. Both operands are
runtime-proven Numbers, so the IEEE compare is spec-exact for all four
operators (IsLooselyEqual defers to IsStrictlyEqual for same-type
operands; NaN == NaN is false, +0 == -0 is true); any non-number
operand declines to the generic coercing path before any side effect.

The dominant win is allocation: a loop test like `i === 50000` was
re-boxing the counter into a JsNumber on every iteration once it
exceeded the small-int cache. LoopDispatchBenchmarks A/B (default job,
adjacent runs):

| row             | base                | lanes               |
|-----------------|---------------------|---------------------|
| StrictEqualTest | 5.967 ms / 2,808 KB | 3.710 ms / 3.26 KB  |
| LooseEqualTest  | 5.523 ms / 2,808 KB | 3.777 ms / 3.27 KB  |

-38% / -32% time, -99.9% allocation (Gen0 eliminated). Untouched rows
byte-identical alloc; time deltas were sign-mixed and traced to window
drift via an A/B/A re-run on identical bits.

New LoopDispatch rows (StrictEqualTest/LooseEqualTest/ModuloEqualTest)
pin the shapes; ModuloEqualTest (the stopwatch.js if-chain shape) is
deliberately not covered yet - its left side is a modulo expression,
not an identifier. EqualityLaneTests pin NaN/signed-zero semantics,
mid-loop type-change decline, BigInt/mixed-type bail and value
positions.

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