Skip to content

Guarded builtin-named method dispatch is 3-4x Node on dayjs/decimal-shaped receivers (#10476 follow-up) #10594

Description

@proggeramlug

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_shape baseline crashed (RangeError: Invalid time value) before completing the iterations.
  • money_shape baseline computed the wrong answer (600000 where Node and the fix both give 1000000),
    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.push was 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindingsperformanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions