Skip to content

perf(runtime): BigInt division fast paths — pi's #1 startup symbol, 33.8x (67.6x → 2.0x of node) - #9141

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf-bigint-pow2-mod
Aug 30, 2026
Merged

perf(runtime): BigInt division fast paths — pi's #1 startup symbol, 33.8x (67.6x → 2.0x of node)#9141
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf-bigint-pow2-mod

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Found by the first named-symbol profile of pi's native startup: perry_runtime::bigint::arith::unsigned_div_limbs was the top CPU symbol at ~17% of samples. Cause: TypeBox's Value.Hash runs, per hashed byte,

var [Prime, Size] = [BigInt("1099511628211"), BigInt("18446744073709551616") /* 2^64 */];
Accumulator = (Accumulator ^ Bytes[byte]) * Prime % Size;

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 is a >> k, O(limbs); (2) single-limb divisor → one hardware u128/u64 per 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):

ms vs node
node v26.5.1 20 1.0x
perry before 1352 67.6x
perry after 40 2.0x

Accumulator 11550294541936438046 byte-equal to node in every run; before/after built from an exact stash/unstash of the same tree with perry-auto-* cleared and archives confirmed rebuilt.

Validation: runtime suite 2829 passed / 0 failed (run twice, no SIGABRT); test_gap_9092_bigint_pow2_mod.ts byte-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, % 0n throwing 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. Added div_mod_cross_check_against_i128_all_tiers: 12,000 randomized differential cases against host i128 across all tiers and sign combinations, mutation-tested (clearing one mask bit makes it fail). Also replaced an inherited expect() on the divisor-nonzero path — a panic inside an extern "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

    • Improved BigInt division and remainder calculations across power-of-two, small, and large values.
    • Corrected handling for zero values, negative operands, wide numbers, and results near the 1024-bit range.
    • Prevented incorrect behavior for division by zero while preserving expected error handling.
    • Improved performance by using optimized calculation paths instead of a fixed-length iteration.
  • Tests

    • Added comprehensive coverage for BigInt quotient and remainder behavior, including randomized and cross-limb scenarios.

@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a1d7ac79-9d2e-4c02-8c68-49dbee8d1a04

📥 Commits

Reviewing files that changed from the base of the PR and between 4994c77 and 798ccca.

📒 Files selected for processing (3)
  • crates/perry-runtime/src/bigint/arith.rs
  • crates/perry-runtime/src/bigint/tests.rs
  • test-files/test_gap_9092_bigint_pow2_mod.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

Changes

The 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

Layer / File(s) Summary
Unsigned division dispatch
crates/perry-runtime/src/bigint/arith.rs
unsigned_div_limbs now detects significant bit lengths and selects power-of-two, single-limb, or bounded multi-limb division paths. It handles zero and smaller dividends explicitly.
Runtime division validation
crates/perry-runtime/src/bigint/tests.rs
Rust tests cover each division path, signed remainder behavior, canonical zero, edge cases, and 3000 randomized comparisons with i128 arithmetic.
BigInt integration regression coverage
test-files/test_gap_9092_bigint_pow2_mod.ts
The regression test covers wide and cross-limb operands, mixed signs, division identities, zero division, hashing, and power-of-two divisor widths.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 798cc

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: thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the BigInt division fast paths and communicates the primary performance improvement. It is detailed but remains directly related to the main change.
Description check ✅ Passed 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 explicitl…
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 3 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

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)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… 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
@proggeramlug

Copy link
Copy Markdown
Contributor Author

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, run_lint_gates.sh all 60 gates passed, git diff origin/main --diff-filter=D empty.

The train initially also carried #9140 (tombstone reuse for small-object churn), which failed four delete/shape-transition tests on their own premise (test premise: the delete did not compact the slots). Dropping it made the rest green, so those failures are its alone — handed back separately. Mentioning it because if any of these three later look implicated in a delete-path regression, #9140 is the change to look at first.

@proggeramlug
proggeramlug merged commit 1ac86e3 into PerryTS:main Aug 30, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant