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.
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 inlinefmul, while0.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 thatshape feeds a
sinC-ABI return into the multiply. Both compilers were builtfrom source identically (
cargo build --release -p perry); source-0.5.1220matches 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)
acc += …, 3M iters)Math.sqrt(i) * Math.sin(i*0.001)Math.sqrt(i) + Math.sin(i*0.001)Math.sqrt(i) * Math.sqrt(i+1)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*sqrtalso goes through the newboxed 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*sinmixes in thesinC-ABI return.Minimal reproduction
perry compile m.js -o m && ./mDisassembly (root cause)
Inner loop of
work, arm64:fmul d1, d9, d0, plus two lightweight leaf coercionhelpers (fast-return-if-number).
fmulis gone; the multiply goes through a boxedmultiply-helper that makes two
blcalls to a heavier coercion routine(checks object tags
0x7ffa/0x7ffd, can call further in) — a non-leafcall per iteration that re-coerces both operands.
Environment
cargo build --release -p perryfrom tagged source (v0.5.1220 / v0.5.1260 = commit 2e105ca).
Anything else
Matters because mixed-transcendental numeric kernels are a primary use case,
and this is a general de-optimization of
Math.*-result multiplies (theinline-
fmulfast path was lost), not a one-off. Bisecting 1220→1260 againstthis repro plus the asm diff should localize the commit.