Skip to content

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

Description

@proggeramlug

bench_bitwise is 20.4× slower than Node (56956 vs 2787, checksums identical after #7403 made it measure anything at all). The cause is one instruction choice.

Evidence chain

The hot loop contains no runtime helper calls at all — it is pure LLVM arithmetic, so the usual "native-able primitive became a runtime call" story does not apply here. The op mix per iteration:

4 frem     ← the problem
3 fsub  3 fadd  2 fmul  2 sitofp  2 fcmp  1 icmp  1 add

frem is not an aarch64 instruction. It lowers to a libm call:

$ otool -tvV … | grep 'bl.*fmod'
4 × bl  _fmod        # in runArithmeticBenchmark

And the profile says it dominates:

1754 samples  fmod

Four fmod calls per iteration × 10M iterations. The source is ordinary integer arithmetic:

result = result + (a % 1000);
result = result - (b % 1000);
result = result + ((a * 3) % 10000);
result = result - ((b * 2) % 10000);

V8 keeps these as SMIs and uses integer modulo.

The mechanism already exists — and knows this is the problem

stmt/loops.rs's emit_object_array_write_index_i64 emits srem, and its doc comment names the exact issue:

"the counters' native i32 registers drive i64 add/sub/mul/srem directly: no float round-trip and, critically, no frem (which lowers to an fmod LIBRARY CALL on AArch64 — ~10ns per element). srem equals JS % on the proven domain (nonnegative dividend, positive divisor)."

So the correctness argument is already worked out and the emitter is already written.

Why it does not fire here

The integer path is reachable only through ObjectArrayWriteNumber, whose variants are OuterCounter | InnerCounter | Constant | Add | Sub | Mul | Mod — the #6812 object-array-write index pattern. a and b in bench_bitwise are loop-carried locals (a = a + 1, reset by a conditional), not loop counters, so they never enter that domain and fall to the f64 emitter's frem.

Every other BinaryOp::Mod site emits frem unconditionally: expr/mod.rs:2240, expr/binary.rs:650, codegen/typed_abi/lower.rs:56 and :433, lower_call/scalar_method.rs:174.

Suggested direction

Widen the integer-domain proof from loop counters to loop-carried locals with a provable integer range. The correctness side-condition is already stated (nonnegative dividend, positive divisor) and the emitter already exists; what is missing is the analysis that admits more values into the proven domain.

This is the same lever #7296 proved worth 9.9× on matmul — see the engine plan's sequencing item 2, where reducing what must be treated as an opaque f64 pulls size and speed together.

Caveat

Timings taken at host load ~4–25. The 20.4× ratio and the 1754/fmod profile share are far outside that noise; exact figures want a quiet machine.

Related: #7396 (array-store guard siting), #7403 (this benchmark measured nothing until today), #7294 (engine plan, Part 2).

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