Skip to content

[perf regression] 0.5.1260 routes Math.*-result multiplies through a re-coercing boxed multiply-helper instead of inline fmul (~1.45x on sqrt(x)*sin(y)) #6511

Description

@dlee

What happened

Between 0.5.1220 and 0.5.1260 the codegen for a floating-point multiply whose
operands are Math.* results changed: 0.5.1220 emits an inline fmul, while
0.5.1260 routes the multiply through a boxed multiply-helper that re-coerces
both operands (two extra non-leaf calls per iteration). The wall-clock cost
surfaces on Math.sqrt(x) * Math.sin(y)~1.45x slower — because that
shape feeds a sin C-ABI return into the multiply. Both compilers were built
from source identically (cargo build --release -p perry); source-0.5.1220
matches the npm 0.5.1220 binary exactly, so this is a genuine version
regression, not a build difference.

Measurements (alternating runs, min ms; ratio is the portable figure)

shape (acc += …, 3M iters) 0.5.1220 0.5.1260 ratio
Math.sqrt(i) * Math.sin(i*0.001) 19–20 27–29 ~1.45x
Math.sqrt(i) + Math.sin(i*0.001) 12–14 12–13 ~1.0
Math.sqrt(i) * Math.sqrt(i+1) 19 19 ~1.0

Holds at 10M iters too (68 → 97 ms, 1.43x, non-overlapping spreads), so it's
not timer quantization. Codegen is deterministic (same-compiler recompiles are
byte-identical in the code section). Same checksum across all variants.

Note the negative controls are symptoms, not proof the change is
product-specific: per the disassembly sqrt*sqrt also goes through the new
boxed helper on 0.5.1260 — it just doesn't show a wall-clock delta because its
operands stay as cheap register doubles the coercion fast-path handles, whereas
sqrt*sin mixes in the sin C-ABI return.

Minimal reproduction

function work() {
  var acc = 0;
  for (var i = 0; i < 3000000; i++) acc += Math.sqrt(i) * Math.sin(i * 0.001);
  return Math.round(acc * 1e-3);
}
work();
var best = 1e9, c;
for (var r = 0; r < 6; r++) { var t = Date.now(); c = work(); var d = Date.now() - t; if (d < best) best = d; }
console.log(best + " ms (chk=" + c + ")");   // ~1.45x slower on 0.5.1260 than 0.5.1220
perry compile m.js -o m && ./m

Disassembly (root cause)

Inner loop of work, arm64:

  • 0.5.1220: inline fmul d1, d9, d0, plus two lightweight leaf coercion
    helpers (fast-return-if-number).
  • 0.5.1260: the fmul is gone; the multiply goes through a boxed
    multiply-helper that makes two bl calls to a heavier coercion routine
    (checks object tags 0x7ffa/0x7ffd, can call further in) — a non-leaf
    call per iteration that re-coerces both operands.

Environment

  • Perry versions: 0.5.1220 vs 0.5.1260, both cargo build --release -p perry
    from tagged source (v0.5.1220 / v0.5.1260 = commit 2e105ca).
  • Host OS: macOS 27.0 (arm64). Same runtime archives / clang for both.

Anything else

Matters because mixed-transcendental numeric kernels are a primary use case,
and this is a general de-optimization of Math.*-result multiplies (the
inline-fmul fast path was lost), not a one-off. Bisecting 1220→1260 against
this repro plus the asm diff should localize the commit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions