Repository navigation
jitv2: count every retired instruction the way the interpreter does - #439
Merged
Merged
Conversation
iris counts every guest instruction executed. The count drives the guest's CP0 Count timer and, under ci_clock, guest timing. The interpreter adds 1 per instruction. Compiled code adds the instructions up while compiling and writes the total to the counter only where a block exits or a branch lands. A path that leaves without writing the total loses those instructions. Two paths did that: - A branch-likely that is not taken. It annuls its delay slot, and the code that wrote the total sat in the slot. The not-taken path dropped every instruction since the last write. fpu/scalar_d counted 17 per loop pass instead of 26. - An exception or bail-out (pending interrupt, busy bus) in the middle of a block. It returned without writing the total so far. Write the pending total on both paths. The writes run only on those exits, so no per-instruction work or new branch is added.
iris counts every guest instruction executed, and the count drives the guest's CP0 Count timer. Compiled code adds instructions up while compiling and writes the total to the counter at certain points. For a block's first instruction that write is sometimes part of the instruction's own code: a CP0 instruction, or an instruction that branches back to the top of a loop. When the run loop jumps into a block from outside, it skipped the part of the first instruction that does the counting. A TLB refill handler starts with a CP0 read and is entered this way on every TLB miss. sys/tlb_miss counted 25 instructions per refill instead of 26. Put the counting where both ways into the block run it. Nothing is added per instruction. The same code moves one step later.
jamesbraid
force-pushed
the
jitv2-icount
branch
from
November 7, 2026 02:51
92f8fb2 to
600fb99
Compare
jamesbraid
marked this pull request as ready for review
November 7, 2026 04:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
iris counts every guest instruction executed. That count drives the guest's CP0 Count timer and, under
ci_clock, guest timing. The interpreter adds 1 per instruction. Compiled code avoids a store per instruction. It adds instructions up while compiling and writes the total to the counter only at certain places: where a compiled block exits and where a branch lands. If execution leaves by a path that doesn't write the total, those instructions are never counted.Three such paths made compiled code count less than the interpreter. Guest timing then depended on when a region finished compiling. With this change both engines count the same. An instruction counts once when it executes, including delay slots and a faulting instruction. An annulled branch-likely slot does not count.
Branch-likely, not taken
A branch-likely is the MIPS branch that skips its delay slot when not taken. The code that wrote the total sat in the delay slot, which only the taken path runs. The not-taken path dropped every instruction since the last write. A loop in fpu/scalar_d counted 17 instructions per pass instead of 26. The not-taken path now starts from the count at the branch and writes it where it leaves the block or lands on a point that expects a written total.
Exception or early exit mid-block
An exception, a pending interrupt or a busy bus in the middle of a block returned without writing the total so far. These exits now write it.
First instruction of a block, entered from the run loop
The code that counts a block's first instruction sat on the path taken by branches inside the block. When the run loop jumped in from outside, it skipped that code. This only matters when the first instruction writes the total itself: a CP0 instruction, or one that branches back to a loop top. The TLB refill handler starts with
dmfc0and is entered this way on every TLB miss. sys/tlb_miss counted 25 instructions per refill instead of 26. The counting now sits where both entries run it.None of this adds work per instruction or a new branch on the hot path. The extra writes run only on these exit paths.
Effect on counts
Deterministic mode (
iris-bench run --deterministic: synchronous compile, guest clock), R10000. Guest instructions per kernel iteration, for the 12 kernels that differed by more than 0.1%:On 45b7f74, 30 of 46 kernels counted differently compiled. The other 18 were off by about one instruction per timed run. On this branch all 46 match the interpreter exactly. Checksums are unchanged.
Guest MIPS is computed from this counter, so it rises for these kernels with no change in speed. The speed comparison below uses work-unit rates instead.
Performance
Ship builds of 45b7f74 and this branch, R10000, bare machine, interleaved real-clock runs pinned to one CCD. Work units per second, median, Mann-Whitney. No kernel is slower at p < 0.05 in either layout. Only codec/rle moves in both layouts, 29% faster, which these measurements do not explain. Every other significant result appears in one layout only, which is the signature of code placement rather than this change.
The other 38 kernels are within noise in both layouts. "Equal alignment" builds every function on a 64-byte boundary, to take host code placement out of the comparison.
Host instructions retired over the whole deterministic suite (
perf stat, n=2 each): 517.49 G on 45b7f74 and 514.27 G on this branch, spread under 0.002% within each build. This is not a like-for-like cost comparison. The guest clock now advances faster on kernels that used to under-count, so the suite runs 6.8% fewer iterations (35.1 M against 37.7 M) for 2.0% more guest instructions.Tests
New tests in
src/cpu/jitv2/equiv_test.rsrun hand-assembled code through the real dispatch path on both engines. Each requires identical state and an exact hand-computed count:loops_with_likely_branches_count_like_the_interpreter: loops with a not-takenbeqlandbc1tl, abeqlto its own fall-through word (taken and not), and abnelback edge falling out of the loop.exception_mid_run_counts_like_the_interpreter: an overflowingaddafter twoaddiuin one region counts 3.region_entered_at_a_flushing_word_counts_it: a refill-handler-shaped region (dmfc0entry,dmtc0, stopping attlbwr), and an entry that falls into a loop top.All fail without their fix: beql loop 302 against 502, exception 0 against 3, refill handler 6 against 7, loop-top entry 11 against 12.
cargo test --features jitv2 --libpasses in release and debug (2959 tests).cargo checkis clean withlightning,jitv2_lockstepandjitv2_opcodefusion.