perf(runtime): BigInt division fast paths — pi's #1 startup symbol, 33.8x (67.6x → 2.0x of node) - #9141
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review. 📝 WalkthroughWalkthroughChangesThe BigInt runtime replaces fixed-width unsigned division iteration with dispatch based on operand shape. It adds power-of-two shifts, single-limb hardware division, bounded multi-limb division, and explicit edge handling. Rust and TypeScript tests cover signed semantics, identities, wide values, and zero division. BigInt division
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to The PR adds localized BigInt division fast paths while preserving existing arithmetic behavior and broad test coverage. No actionable merge-blocking risk remains beyond normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Full details: Description checkExplanation The description provides a detailed summary, implementation changes, benchmark results, validation results, and follow-up scope. It does not use the repository template headings and does not explicitly provide a related issue, checklist confirmations, or screenshots/output section, but the core required information is complete. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… long division
`unsigned_div_limbs` ran a bit-at-a-time binary long division over all 1024
bits for every BigInt `/` and `%` that escaped the both-fit-in-i64 fast path.
The named-symbol profile of pi's real startup had it as the TOP CPU symbol
(~17% of samples): TypeBox's `Value.Hash` runs an FNV-1a accumulator that does
`(Accumulator ^ Bytes[byte]) * Prime % 18446744073709551616n` per hashed byte,
and `x % 2^64n` is exactly the shape that path handles worst — node masks.
Three tiers ahead of the general loop, all on magnitudes with the sign
re-applied by the caller, so ECMA BigInt::remainder semantics are unchanged
(sign follows the DIVIDEND, `(-7n) % 4n === -3n`, canonical zero, never -0n):
0. Zero dividend, or dividend shorter than the divisor: return directly
(quotient 0, remainder = dividend).
1. Power-of-two divisor (single set bit at k): remainder = the low k bits of
the dividend, quotient = dividend >> k. O(limbs). k == 0 is divisor 1.
2. Single-limb divisor: one hardware u128/u64 per dividend limb, top-down —
covers `% prime` for the FNV prime and friends.
3. The general bit loop, now bounded by the dividend's actual bit length
instead of a fixed 1024 iterations.
A zero divisor can no longer reach here (both callers throw RangeError first),
so that case debug-asserts and returns rather than unwinding out of an
`extern "C"` frame.
FNV1A64 over 200k bytes, min of 3, quiet Mac mini (load 1.4):
node v26.5.1 20 ms 1.0x
perry before 1352 ms 67.6x
perry after 40 ms 2.0x (33.8x faster; accumulator byte-equal to node)
Validation: full `perry-runtime --lib` suite green (2829 passed, 0 failed);
a new 12,000-case randomized differential of `js_bigint_div`/`js_bigint_mod`
against host i128 arithmetic, cycling divisor shapes so all three tiers are
covered under all four sign combinations; and
`test-files/test_gap_9092_bigint_pow2_mod.ts` byte-identical to node both
before and after the change.
Claude-Session: https://claude.ai/code/session_01Ay8VyLkKbm8Hkc1xmvTEsP
798ccca to
292d624
Compare
|
Merged via a merge train — cherry-picked with two other PRs onto one branch and validated together in a single build. Final validation: hir 365 passed, codegen 1356, runtime 2831 passed (exit 0, 0 abort markers), perry --bins 1066, The train initially also carried #9140 (tombstone reuse for small-object churn), which failed four delete/shape-transition tests on their own premise ( |
Found by the first named-symbol profile of pi's native startup:
perry_runtime::bigint::arith::unsigned_div_limbswas the top CPU symbol at ~17% of samples. Cause: TypeBox'sValue.Hashruns, per hashed byte,so every byte of every schema hashed at startup took a full limb-division loop where node masks.
Four tiers in
unsigned_div_limbs(magnitudes only; signs re-applied by callers, untouched): (0) zero dividend or dividend < divisor returns directly; (1) power-of-two divisor → remainder is the low k bits, quotient isa >> k, O(limbs); (2) single-limb divisor → one hardwareu128/u64per dividend limb; (3) the original bit loop, now bounded by the dividend's real bit length instead of a fixed 1024. Tiers only add early exits and shrink tier 3's bound, so there is no regression path.Benchmark — FNV1A64 over 200k bytes, min of 3, quiet Mac mini (the dev box was at load 52-144 from other agents and unusable for timing):
Accumulator
11550294541936438046byte-equal to node in every run; before/after built from an exact stash/unstash of the same tree withperry-auto-*cleared and archives confirmed rebuilt.Validation: runtime suite 2829 passed / 0 failed (run twice, no SIGABRT);
test_gap_9092_bigint_pow2_mod.tsbyte-identical to node both before and after — the before-run is the control proving the baseline was already semantically correct. It covers(-7n)%4n === -3n, negative divisors, canonical zero on exact multiples, dividend < / == divisor, 250-bit dividends % 2^64 / 2^13 / 2^70, a 1022-bit dividend % 2^1021,% 1n,% 0nthrowing RangeError, non-power-of-two single- and multi-limb divisors, a k-sweep across the limb boundary, and a 96-case identity/sign/range sweep. Addeddiv_mod_cross_check_against_i128_all_tiers: 12,000 randomized differential cases against hosti128across all tiers and sign combinations, mutation-tested (clearing one mask bit makes it fail). Also replaced an inheritedexpect()on the divisor-nonzero path — a panic inside anextern "C"frame aborts the process — with a debug assert plus a safe return.Next in this area (not this PR): tier 3 is now the outlier at 154 ns vs node 15 for multi-limb non-power-of-two divisors; Knuth algorithm D would close it.
Implemented by subagents in an isolated worktree (across two sessions after a rate-limit interruption); reviewed and shipped by the coordinating session.
Summary by CodeRabbit
Bug Fixes
Tests