Repository navigation
Guarded builtin-named method dispatch is 3-4x Node on dayjs/decimal-shaped receivers (#10476 follow-up) #10594
Description
Follow-up to #10476 / PR #10591, which fixes a real correctness bug: a method call whose name matched a
Date/Number/Array.prototype builtin was lowered straight to that builtin regardless of the receiver, so a
user's own toISOString / toFixed / getTime was silently skipped.
The fix guards that dispatch on the receiver's runtime kind. For a receiver the compiler has statically proven
to be a Date/number/array, nothing changes. For any other receiver, the receiver is evaluated once, rooted, and
branched on at runtime. That guarded path is 3–4x slower than Node.
Measurements (200k iterations, perf stat -e instructions, 3 runs, from PR #10591)
| shape | fix instructions | fix wall (3 runs) | Node wall (3 runs) | fix vs Node |
|---|---|---|---|---|
dayjs_shape |
~4.80B | 651 / 675 / 678 ms | 187 / 204 / 240 ms | ~3.1–3.3x slower |
money_shape |
~1.30B | 363 / 417 / 418 ms | 102 / 104 / 107 ms | ~3.6–4.0x slower |
Control shapes (date_typed, fixed_typed, fixed_any, ends_typed, sorted_typed, date_any) stay within
~2% of baseline, so the cost is confined to the guarded path.
Why there is no "regression vs baseline" number
The pre-fix baseline is not a valid comparator for these two shapes:
dayjs_shapebaseline crashed (RangeError: Invalid time value) before completing the iterations.money_shapebaseline computed the wrong answer (600000where Node and the fix both give1000000),
because it dispatched to a numeric builtin instead of the user's method.
The old speed was the bug. Only fix-vs-Node is meaningful here.
Why it matters
This is the shape of every decimal/money/date-wrapper library — dayjs's toISOString/toJSON,
decimal.js and bignumber.js's toFixed — i.e. exactly the packages the compile-the-real-package work targets.
The project's bar is to match Node with 20% below acceptable; 3–4x is well outside it.
Where to look
crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard.rs— the guard itself.- The per-call receiver re-evaluation and rooting is the obvious first suspect; compare with perf(runtime): stop re-deriving the receiver on every array push (−74%) #10414, where
Array.prototype.pushwas re-deriving its receiver 6x per call and fixing it was worth −74.6%. - A monomorphic inline cache on the receiver kind at the call site would likely collapse most of this: these
loops call the same user method on the same shape every iteration.
Fixtures used for the numbers are the dayjs_shape / money_shape cases in PR #10591's perf section.
Activity
proggeramlug commentedon Sep 18, 2026
Accepted as a known cost (2026-09-18): the correctness fix ships, and this regression is handed to the
in-flight performance work rather than blocking it. There is deliberately no "restore the old speed" option —
the old speed came from the buggy path.
Highest-value lead recorded above: the per-call receiver re-derivation. #10414 was the same shape of defect in
Array.prototype.push(receiver re-derived 6x per call) and fixing it was worth −74.6%. A monomorphic inlinecache on receiver kind at the call site is the obvious first thing to try, since these loops call the same
method on the same shape every iteration.