Skip to content

perf(codegen): route % on i64-range integer locals to srem, not fmod - #7416

Merged
proggeramlug merged 2 commits into
mainfrom
perf/7404-i64-integer-locals
Aug 5, 2026
Merged

perf(codegen): route % on i64-range integer locals to srem, not fmod#7416
proggeramlug merged 2 commits into
mainfrom
perf/7404-i64-integer-locals

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Closes #7404. bench_bitwise: 20.4× slower than Node → 1.39×. 55108ms → 3763ms, a 14.6× improvement, with the Node-verified CHECKSUM:525000000 unchanged.

The bug

frem is not an aarch64 instruction — it lowers to an fmod library call. The hot loop emitted four per iteration (1754 profile samples in fmod).

A guarded srem fast path already existed. Every gate in front of it asked the wrong question: is_integer_valued_expr resolves a LocalGet through integer_locals, which is an i32-range property that also gates i32 shadow slots. Widening that would have put an i32-overflowing value into an i32 slot — a miscompile. The % path converts to i64 and only needs integer-valued-within-i64.

collect_int_valued_i64_locals supplies that weaker property as a magnitude lattice, read only by the % gate. integer_locals is untouched.

Three holes closed, each silently-wrong arithmetic

hole why it mattered
Mul blowup a pure integrality predicate let (a*b*c) % n reach fptosi … to i64 with a 93-bit product — poison. The lattice gates at 2^62, which also closes the same pre-existing hole for i32 locals
Zero divisor an early version routed 1000 % d where d decrements through zero. srem(x,0) is UB; JS needs NaN. Divisor now restricted to a non-zero integer literal
The saturation argument "±constant can't leave i64 in finite time" is falsea = a + 1e18 escapes in ~10 iterations. Replaced with an IEEE-754 bound: once ulp(v) ≥ 4D, v ± d rounds back to v exactly, so |L| ≤ 2^(55+log₂D) forever

That third one was my error in the task brief, caught and corrected during implementation.

The gate that actually decides this

Not binary.rs:588. There are six frem emission sites, and the owner here is expr/mod.rs::lower_numeric_binary_value, which intercepts numeric binary ops before binary::lower and only handed Mod off when the dividend had an i32 counter slot. Widening the predicate alone changed nothing — the IR acceptance test caught that and stayed red.

Verification

  • IR: hot fn frem 4 → 0, srem 0 → 4 (baseline had zero srem, so the check could genuinely fail)
  • Correctness: edge-case differential incl. both -0 cases via Object.is (not ===, which cannot distinguish them) — byte-identical to Node 26.5.1
  • Oracle: CHECKSUM:525000000 on all runs
  • Speed: min 55108 → 3763; Node 2708. Load 1.95–2.8
  • Regressions: cargo test -p perry-codegen --lib 632 passed / 0 failed; 26/27 math/number/int gap tests, the one failure (process.setTraceSigInt arg validation) pre-existing and byte-identical between arms
  • cargo fmt --check clean; file-size cap and addr-class ratchet pass

No env knob added, per CLAUDE.md's kill-policy.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected JavaScript remainder (%) calculations for integer-valued values outside the 32-bit range.
    • Prevented unsafe integer remainder optimizations when the divisor may be zero.
    • Improved numerical correctness while preserving optimized execution for proven-safe integer cases.
  • Tests

    • Added regression coverage for large integer locals and potentially zero divisors.
    • Refined garbage-collection evacuation checks to rely on liveness results without requiring diagnostic output.

Ralph Küpper added 2 commits August 5, 2026 07:31
frem is not an aarch64 instruction -- it lowers to an fmod LIBRARY CALL.
bench_bitwise emitted four per iteration and ran 20.4x slower than Node.
It is now 1.39x: 55108ms -> 3763ms, a 14.6x improvement, with the
Node-verified CHECKSUM:525000000 unchanged.

A guarded srem fast path already existed. Every gate in front of it asked
the wrong question: is_integer_valued_expr resolves a LocalGet through
integer_locals, which is an i32-RANGE property that also gates i32 shadow
slots -- widening it would have put an i32-overflowing value into an i32
slot. The % path converts to i64 and only needs integer-valued-in-i64.

collect_int_valued_i64_locals supplies that weaker property as a
magnitude lattice, read only by the % gate. integer_locals is untouched.

Three holes closed while building it, each silently-wrong-arithmetic:

  * Mul blowup: a pure integrality predicate let (a*b*c) % n reach
    fptosi..to i64 with a 93-bit product -- poison. The lattice gates at
    2^62, which also closes the same pre-existing hole for i32 locals.
  * Zero divisor: an early version routed 1000 % d into srem where d
    decrements through zero. srem(x,0) is UB; JS needs NaN. The divisor
    is now a non-zero integer literal.
  * The saturation argument was wrong for large deltas -- a = a + 1e18
    escapes i64 in ~10 iterations, not 2^63. Replaced with an IEEE-754
    bound: once ulp(v) >= 4D, v +/- d rounds back to v exactly.

The deciding gate is expr/mod.rs::lower_numeric_binary_value, which
intercepts numeric binary ops before binary::lower and only handed Mod
off for i32 counter slots. There are six frem emission sites; widening
the predicate alone changed nothing and the IR acceptance test caught it.

Verified: hot fn frem 4->0, srem 0->4; edge-case differential incl. both
-0 cases via Object.is is byte-identical to Node 26.5.1; cargo test
-p perry-codegen --lib 632 passed; 26/27 math/number/int gap tests, the
one failure pre-existing and unrelated.
@proggeramlug
proggeramlug merged commit 1e838b4 into main Aug 5, 2026
@proggeramlug
proggeramlug deleted the perf/7404-i64-integer-locals branch August 5, 2026 05:31
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38ef5e8a-6d8d-4fda-a3cd-c0e5085f7100

📥 Commits

Reviewing files that changed from the base of the PR and between 9f63aa1 and 63c7791.

📒 Files selected for processing (17)
  • .github/workflows/gc-native-roots.yml
  • changelog.d/7414-macos-rs4gc-inprocess-gc-diag.md
  • changelog.d/7415-dominance-corpus.md
  • changelog.d/7416-i64-integer-locals.md
  • crates/perry-codegen/src/codegen/closure.rs
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/codegen/function.rs
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/collectors/hir_facts.rs
  • crates/perry-codegen/src/collectors/int_valued_i64_locals.rs
  • crates/perry-codegen/src/collectors/mod.rs
  • crates/perry-codegen/src/expr/binary.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/type_analysis.rs
  • crates/perry-codegen/src/type_analysis/numeric.rs
  • crates/perry-codegen/src/type_analysis/numeric/tests.rs
  • scripts/gc_evacuation_liveness_assert.py

📝 Walkthrough

Walkthrough

The PR adds bounded i64 local analysis for safe integer % lowering and propagates these facts through code generation. It also removes collector-diagnostic requirements from native-roots GC evacuation validation.

Changes

Integer modulo lowering

Layer / File(s) Summary
Bounded i64 local analysis
crates/perry-codegen/src/collectors/int_valued_i64_locals.rs, crates/perry-codegen/src/collectors/hir_facts.rs, crates/perry-codegen/src/collectors/mod.rs
The collector identifies eligible i64-range locals, records magnitude bounds, rejects unsupported writes, and validates nested control flow with tests.
Fact graph and codegen context wiring
crates/perry-codegen/src/codegen/*.rs, crates/perry-codegen/src/expr/mod.rs
Native facts populate FnCtx::int_valued_i64_locals for functions, closures, entry modules, and methods.
Numeric analysis and modulo lowering
crates/perry-codegen/src/type_analysis/numeric.rs, crates/perry-codegen/src/type_analysis.rs, crates/perry-codegen/src/expr/binary.rs, crates/perry-codegen/src/expr/mod.rs, crates/perry-codegen/src/type_analysis/numeric/tests.rs, changelog.d/7416-i64-integer-locals.md
Magnitude analysis admits safe i64 values. Divisor analysis excludes potentially zero locals. Eligible modulo expressions lower to srem; unsafe cases retain floating-point remainder lowering.

GC evacuation liveness validation

Layer / File(s) Summary
Evacuation diagnostic requirement removal
.github/workflows/gc-native-roots.yml, scripts/gc_evacuation_liveness_assert.py
The workflow no longer sets PERRY_GC_DIAG, and the assertion script no longer rejects traces without collector diagnostics.

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

Sequence Diagram(s)

sequenceDiagram
  participant HIRFacts
  participant TypeFacts
  participant FnCtx
  participant NumericAnalysis
  participant ModuloLowering
  HIRFacts->>TypeFacts: store bounded i64 local facts
  TypeFacts->>FnCtx: initialize int_valued_i64_locals
  FnCtx->>NumericAnalysis: analyze modulo operands
  NumericAnalysis->>ModuloLowering: approve safe integer remainder
  ModuloLowering->>ModuloLowering: emit srem i64
Loading

Possibly related issues

Possibly related PRs

  • PerryTS/perry#7338 — It introduced the GC diagnostic requirements removed here.
  • PerryTS/perry#7414 — It directly covers the same GC workflow and diagnostic-handling changes.
  • PerryTS/perry#6898 — Both changes extend flow-analysis-derived integer-local facts through the code-generation pipeline.

Suggested reviewers: thehypnoo, andrewtdiz

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/7404-i64-integer-locals

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.

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.

perf: the % fast path gates on the i32 set, but only needs integer-valued-in-i64 (20.4× on bench_bitwise)

1 participant