Skip to content

Sound recursive-numeric specialization to recover the ~20% #7238 cost on 05_fibonacci #7244

Description

@proggeramlug

#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.ts fib(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:

  1. 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.
  2. 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.
  3. 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.

Activity

  1. proggeramlug commented on Aug 2, 2026

    @proggeramlug
    ContributorAuthor

    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.

  2. proggeramlug commented on Aug 15, 2026

    @proggeramlug
    ContributorAuthor

    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:

    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 zero js_* 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

    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.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions