You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Sound recursive-numeric specialization to recover the ~20% #7238 cost on 05_fibonacci #7244
#7238 removed emit_i64_specializations because neither half of its contract
was provable: a number parameter is a double (the wrapper's fptosi
truncated fractional arguments), and nothing bounded any intermediate at 2^53
(where JS starts rounding and exact i64 arithmetic does not).
That removal has a measured cost. Interleaved A/B on a Mac mini, 9 pairs,
fixed arm slower in 9/9: benchmarks/suite/05_fibonacci.tsfib(40) went
450 ms → 555 ms, about 20%. 14_closure was within noise because its compute picked up a __typed_f64 clone instead; fib did not, because its
body is not straight-line and so does not qualify for the typed-f64 gate.
(Neither host was quiet — indicative, not controlled.)
The shape a sound version needs, which #7238 was not the place to build:
Guarded entry. The public f64 wrapper proves each argument is an exact
integer (sitofp(fptosi(x)) == x, and in range) before entering the
integer body, instead of assuming it.
A deopt edge. The ordinary f64 body must still be emitted — the removed
pass suppressed it, which is precisely why the wrapper had nowhere to fall
back to. A failed entry guard calls the f64 body.
A bounded interior, or a checked one. Either a static magnitude bound
in the style of i32_chain_magnitude_bits (fix(codegen): round JS arithmetic to f64 at every i32-chain step (#7232) #7237) — which needs a bounded
leaf, and a self-recursive parameter has none — or a per-operation |v| <= 2^53 check that deopts to the f64 body with the current (still
exact) arguments. The per-operation check is the only one that covers the
recursive accumulator, and it needs measuring: three branches per fib
call may well erase the 20% it is trying to recover.
Worth noting that the straight-line half of this is already solved by the
typed-ABI clones and the Phase-2 specialized ABI, which prove representations
from call sites rather than assuming them. The open ground is specifically self-recursive numeric functions whose body is not straight-line, which is
the one case where fib lands and no existing specializer reaches.
Acceptance: 05_fibonacci back to within noise of f8f1e7188, with test-files/test_gap_7238_i64_specialization_exactness.ts still byte-exact
against the pinned Node — that test already contains the fractional-argument,
2^53-boundary and past-2^63 shapes any such pass has to survive.
Two caveats on the measurement above, for whoever picks this up: both arms were built --profile perry-dev (opt-level=1, no LTO), so the ~20% is an arm-to-arm ratio at that profile and a release build could narrow or widen it — unmeasured. And neither host was quiet. For scale, Node 26.5.1 runs the same fib(40) in ~1500 ms on that host, so the current (fixed) arm is still ~2.7x faster than Node; the i64 arm was ~3.3x. The "about 2x faster than Node.js" claim in docs/src/getting-started/hello-world.md — which uses this exact benchmark — therefore still holds, and needs no edit.
Closing — #8167 delivers exactly this, and the measured recovery far exceeds the ~20% asked for.
This issue asked for sound recursive-numeric specialization after #7238 removed emit_i64_specializations, whose contract was not provable: a number parameter is a double (the wrapper's fptosi truncated fractional arguments), and nothing bounded intermediates at 2^53.
#8167 (merged, 499e29627) implements it soundly rather than restoring the old shortcut.
How the contract is discharged
The slot contract is js_typed_i32_arg_guard: finite, integral, in signed-32-bit range, and not -0. A raw-i32 parameter plus integer literals plus +/- discharges three of the four by induction, with the exact-integer cap enforced at every node including leaves — without which 9007199254740993 - 9007199254740992 would "prove" a window the runtime never computes. That is precisely the 2^53 hole this issue named.
The fourth, containment, is not free and is not assumed: n - 1 for an i32 n spans [-2^31-1, 2^31-2], one value wider than the slot. So spec_i32_derived_window answers it explicitly — inside the slot → direct call, disjoint → no diamond at all, overlapping → one range test with the boxed entry as the cold arm.
Mul is deliberately excluded, and that exclusion was measured, not argued: with it admitted, probe(n * 0) for n < 0 prints Infinity where node prints -Infinity, because -0 has no i32 to round-trip through.
Measured
Same source, same machine, /usr/bin/time -l, instructions retired:
45.3x. This issue asked to recover ~20% on fib(40); the delivered recovery is far larger, because the cliff had deepened since this was filed — #8033 later removed the generic body's parameter evidence too, so the specialized clone became the only proof-bearing body and was reachable from exactly one call site in the module.
Output byte-exact (102334155). The clone now self-recurses twice with zerojs_* calls inside it. Codegen suite 1470 passed / 9 failed against main's 1467 / 9 — failure sets identical by name, zero new. Three new tests, one positive and two negatives, each asserting the clone exists before asserting anything about its call sites; sabotage fails in both directions.
What I am NOT claiming
05_fibonacci.ts itself was not re-run. I measured an equivalent four-line fib(40). The mechanism is identical and the margin is 45x rather than 20%, so the conclusion is not close to the line — but if you want the corpus row specifically, it is one benchmark run.
Closing this one because its specific ask — a sound recursive-numeric specialization, with the 2^53 and truncation holes actually addressed — is delivered and measured.
Activity
proggeramlug commentedon Aug 2, 2026
Two caveats on the measurement above, for whoever picks this up: both arms were built
--profile perry-dev(opt-level=1, no LTO), so the ~20% is an arm-to-arm ratio at that profile and areleasebuild could narrow or widen it — unmeasured. And neither host was quiet. For scale, Node 26.5.1 runs the samefib(40)in ~1500 ms on that host, so the current (fixed) arm is still ~2.7x faster than Node; the i64 arm was ~3.3x. The "about 2x faster than Node.js" claim indocs/src/getting-started/hello-world.md— which uses this exact benchmark — therefore still holds, and needs no edit.proggeramlug commentedon Aug 15, 2026
Closing — #8167 delivers exactly this, and the measured recovery far exceeds the ~20% asked for.
This issue asked for sound recursive-numeric specialization after #7238 removed
emit_i64_specializations, whose contract was not provable: anumberparameter is a double (the wrapper'sfptositruncated fractional arguments), and nothing bounded intermediates at 2^53.#8167 (merged,
499e29627) implements it soundly rather than restoring the old shortcut.How the contract is discharged
The slot contract is
js_typed_i32_arg_guard: finite, integral, in signed-32-bit range, and not-0. A raw-i32parameter plus integer literals plus+/-discharges three of the four by induction, with the exact-integer cap enforced at every node including leaves — without which9007199254740993 - 9007199254740992would "prove" a window the runtime never computes. That is precisely the 2^53 hole this issue named.The fourth, containment, is not free and is not assumed:
n - 1for an i32nspans[-2^31-1, 2^31-2], one value wider than the slot. Sospec_i32_derived_windowanswers it explicitly — inside the slot → direct call, disjoint → no diamond at all, overlapping → one range test with the boxed entry as the cold arm.Mulis deliberately excluded, and that exclusion was measured, not argued: with it admitted,probe(n * 0)forn < 0printsInfinitywhere node prints-Infinity, because-0has no i32 to round-trip through.Measured
Same source, same machine,
/usr/bin/time -l, instructions retired:45.3x. This issue asked to recover ~20% on
fib(40); the delivered recovery is far larger, because the cliff had deepened since this was filed — #8033 later removed the generic body's parameter evidence too, so the specialized clone became the only proof-bearing body and was reachable from exactly one call site in the module.Output byte-exact (
102334155). The clone now self-recurses twice with zerojs_*calls inside it. Codegen suite 1470 passed / 9 failed against main's 1467 / 9 — failure sets identical by name, zero new. Three new tests, one positive and two negatives, each asserting the clone exists before asserting anything about its call sites; sabotage fails in both directions.What I am NOT claiming
05_fibonacci.tsitself was not re-run. I measured an equivalent four-linefib(40). The mechanism is identical and the margin is 45x rather than 20%, so the conclusion is not close to the line — but if you want the corpus row specifically, it is one benchmark run.fib'selseedge wheren >= 2already narrows the parameter. Filed as perf(codegen): #8167 leaves 22% on fib40 — the range test is provably unnecessary on a branch that already narrows the parameter #8171 rather than left inside this issue.deeplisthas zero dynamic-arith sites yet is +17.2%. That work lives in perf(codegen): recover guarded ordinary-parameter specialization lost by #8033 #8079 and A guarded $spec_b clone never re-enters itself either — the Tier-B half of #8167 #8169.Closing this one because its specific ask — a sound recursive-numeric specialization, with the 2^53 and truncation holes actually addressed — is delivered and measured.