Repository navigation
Sound recursive-numeric specialization to recover the ~20% #7238 cost on 05_fibonacci #7244
Description
Activity
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.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:build fib(40) instructions carrying the regression 211.54 G #8167 4.67 G pre-#8033 3.83 G 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.- perf(codegen): let a specialized entry re-enter itself #8167 lands 22% short of pre-fix(codegen): require runtime evidence for local binding types #8033 (4.67 vs 3.83 G). That is the range test, provably unnecessary on
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. - The broader fix(codegen): require runtime evidence for local binding types #8033 cliff is not closed by this. Nine corpus rows still sit above their 08-12 level and they are not one mechanism —
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.
#7238 removed
emit_i64_specializationsbecause neither half of its contractwas provable: a
numberparameter is a double (the wrapper'sfptositruncated 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)went450 ms → 555 ms, about 20%.
14_closurewas within noise because itscomputepicked up a__typed_f64clone instead;fibdid not, because itsbody 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:
integer (
sitofp(fptosi(x)) == x, and in range) before entering theinteger body, instead of assuming it.
pass suppressed it, which is precisely why the wrapper had nowhere to fall
back to. A failed entry guard calls the f64 body.
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 boundedleaf, and a self-recursive parameter has none — or a per-operation
|v| <= 2^53check that deopts to the f64 body with the current (stillexact) arguments. The per-operation check is the only one that covers the
recursive accumulator, and it needs measuring: three branches per
fibcall 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
fiblands and no existing specializer reaches.Acceptance:
05_fibonacciback to within noise off8f1e7188, withtest-files/test_gap_7238_i64_specialization_exactness.tsstill byte-exactagainst the pinned Node — that test already contains the fractional-argument,
2^53-boundary and past-2^63 shapes any such pass has to survive.