Skip to content

Add unboxed operand lane for arithmetic binary operators#2664

Merged
lahma merged 3 commits into
sebastienros:mainfrom
lahma:perf-numeric-operand-lane
Jul 13, 2026
Merged

Add unboxed operand lane for arithmetic binary operators#2664
lahma merged 3 commits into
sebastienros:mainfrom
lahma:perf-numeric-operand-lane

Conversation

@lahma

@lahma lahma commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Comparison operators got an unboxed slot-operand lane in #2578 and bitwise operators have BitwiseIdentifierLane, but the arithmetic operators (+ - * /) still evaluate identifier operands through the full boxed read path — JintIdentifierExpression.GetValueMaterializeUnboxedOrNull, re-materializing a JsNumber for every unboxed slot value written by the compound/assignment fast paths. ETW profiling shows Times/Plus as top drivers into the identifier-read lane (~11% inclusive on a SunSpider mix).

This adds NumericOperandLane for id op id, id op numericConst and numericConst op id leaf shapes: both operands are read as raw doubles through the established SlotLocationCache + TryGetNumberSlotForRead + validated-global-descriptor machinery, the op computes on raw doubles, and the result is boxed once by JsNumber.Create.

Three commits:

  1. The lane — pure reads; a decline (non-number binding, TDZ, slot miss) falls to the generic path which replays left-then-right evaluation with no observable double effect, so string concat / valueOf / BigInt / TDZ ordering are untouched (the generic path still owns them). Suspendable frames decline (generator resume replays a saved left operand). Integer-typed results are preserved (Create(double) normalizes integral values to the same Integer-typed instances the boxed arms produce), so downstream integer fast paths stay armed.
  2. Re-admit * in the numeric assignment fast path — the exclusion rationale predated Preserve -0 in integer multiplication fast paths #2620: the boxed integer-multiply arm used to produce +0 for 0 * -5; since Preserve -0 in integer multiplication fast paths #2620 routes zero products through double arithmetic, and an unboxed slot stores -0 exactly, x = a * b can use the same slot lane as + - / %. IntegerMultiplicationPreservesNegativeZero extended with the discard-mode shapes.
  3. Struct embedding — the lane is an embedded struct in Minus/Times/Divide (same precedent as the comparison classes' embedded lane; a separate lane object costs an extra dereference per evaluation, measured at +8.7% on a hit-heavy loop). Plus wraps it in a heap-allocated BoxedNumericOperandLane built only when the shape matches, so the many string-concat Plus nodes carry a single null reference instead of an embedded slot-cache pair.

Depends on the JsNumber.Create(double) fmod fix (#2662) — the lane boxes results through Create(double), which made the fmod-based integral detection a first-order cost.

Benchmarks (default jobs, adjacent A/B vs #2662 branch)

Benchmark before after Δ
FunctionLocalNumberLoop MixedArithmetic 10.57 ms 9.97 ms −5.7%
SunSpider math-cordic 116.06 ms 108.96 ms −6.1%
SunSpider math-spectral-norm 54.22 ms 52.19 ms −3.7%
SunSpider math-partial-sums 52.25 ms 51.50 ms −1.4%
SunSpider access-nbody 76.61 ms 77.34 ms +1.0% (drift band)
PlainNumericAssign SumAssign / DivAssign / Mixed 23.30 / 32.90 / 38.03 ms 23.18 / 32.53 / 39.20 ms neutral
StringBuilder One / Two / Three (s + t decline guard) 2.12 / 2.70 / 3.20 µs 2.17 / 2.69 / 3.19 µs neutral
FunctionLocalNumberLoop AccumulatorWithCallArg 22.51 ms ±0.61 23.10 ms ±1.05 overlapping distributions (launchCount 5; row contains no armed node — bimodal layout noise, swings 21–24 ms across a day)

Gates

  • Jint.Tests: 3537 (net10.0) + 3474 (net472) passed, 0 failed
  • Jint.Tests.PublicInterface: 82 × 2 TFMs passed
  • Jint.Tests.CommonScripts: 28 × 2 TFMs passed
  • Test262: 99,429 passed, 0 failed
  • Semantics smoke: −0 products (Object.is), valueOf called exactly once through the generic path, string concat, TDZ left-then-right ReferenceError ordering, integer-typing chains ((a*b) % 5 stays on the integer remainder path) — verified cold and cache-armed

🤖 Generated with Claude Code

lahma and others added 3 commits July 13, 2026 15:24
Comparison operators (PR sebastienros#2578) and bitwise operators read identifier
operands as raw doubles through slot/global caches, but Plus/Minus/Times/
Divide still materialize JsNumber instances via the full identifier read
path for every evaluation, including re-materializing unboxed slot values.

Add NumericOperandLane covering id-op-id, id-op-const and const-op-id leaf
shapes: both operands read via TryGetNumberSlotForRead (declines strings,
objects, BigInt, TDZ - the generic path then owns coercion and errors),
compute on raw doubles, box the result once. A nullable class rather than
an embedded struct so the many non-numeric Plus nodes pay one reference
field instead of two slot caches.

Raw-double multiply preserves -0 for zero products of opposite signs,
matching the boxed integer arm fixed in sebastienros#2620.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The exclusion predated sebastienros#2620: the boxed integer-multiply arm used to
produce +0 for 0 * -5, so a raw-double lane would have changed observable
results. sebastienros#2620 routes zero products through double arithmetic, and an
unboxed slot stores -0 exactly, so x = a * b can now use the same
slot-to-slot lane as + - / %.

Extends IntegerMultiplicationPreservesNegativeZero with the discard-mode
assignment shapes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The lane object costs an extra dereference per evaluation on hit-heavy
loops (MixedArithmetic +8.7% vs the embedded-struct comparison lanes).
Minus/Times/Divide are almost always numeric and follow the comparison
classes precedent of an embedded lane struct; Plus keeps a heap-allocated
wrapper so string-concat nodes pay one null reference instead of an
embedded slot-cache pair.

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