Repository navigation
Conversation
KSEG0 is unmapped: both translator arms compute its physical address as
`virt_addr & 0x1FFF_FFFF`, with no TLB entry, ASID or generation to
consult. The inline load/store guard still probed the nutlb for it on every
kernel load and store — an index computation, two dependent loads and the
validity-bitmask load — and a memory hot-path study measured that guard as
the bulk of a 15.8 ns L1 load.
The guard now tests one thing first:
(virt_addr >> 29) == core.jit_kseg0_rule -> phys = va & 0x1FFF_FFFF
else -> the nutlb probe, unchanged
`0xFFFF_FFFF_8800_0000 >> 29` is `0x7_FFFF_FFFC` — bits 63:32 all ones and
bits 31:29 = 0b100 — so a single compare covers both halves of the address
test. That is exactly `translate_64bit_impl`'s xkseg compatibility arm, the
only route to this translation in 64-bit mode. In 32-bit mode the translator
ignores bits 63:32, so the same compare accepts a conservative subset of
what it translates (it demands the sign-extended form, which `lui`/`la`
produce); anything else falls through to the nutlb probe.
`jit_kseg0_rule` is derived state: compiled code reads it through
`offset_of!`, and only Rust writes it. `refresh_kseg0_direct_rule`
recomputes it from `Status` and `Config` at the two hooks those registers
already own — `resync_privilege_state` (every Status change, by that
function's own documented invariant) and `handle_cp0_side_effects` (every
`MTC0 Config`, the same hook the K0 nutlb flush already rests on).
`kseg0_direct_rule` is the executable specification: kernel mode only
(EXL/ERL force it, else KSU == 0) and `Config.K0` must select a cached
algorithm in the *model's* table, so an uncached KSEG0 still goes to the
bus. When either fails the rule is `1 << 63`, which `virt_addr >> 29` cannot
produce, so every access in that state takes the nutlb probe as before.
Everything downstream of `phys` — L1-D tag, dirty bit, LRU, tcache mapped
test, swizzle, store generation bump — is the same code on both arms, so
the cache model, VCE and aliasing are untouched. Only how `phys` was
obtained differs.
A predicate evaluated per access (two CP0 loads, a dozen ALU ops) measured
worse than the probe it replaced, and hoisting it to region entry measured
worse still: an access in the tcache path is only a handful of host
instructions, and `iris-bench` enters regions often enough that per-entry
work costs more than it saves. One memory-operand compare is the shape that
wins.
Measured on `iris-bench run --cpu r4400` (in-process, `lightning,jitv2`,
median of three interleaved runs per binary, ns per work unit):
mem/latency_l1 8.81 -> 5.28 (1.67x) mem/copy 1.44 -> 0.99 (1.45x)
mem/fill 1.58 -> 0.98 (1.61x) mem/random 100.7 -> 51.3 (1.96x)
mem/latency_dram 88.9 -> 81.2 (1.09x) int/dhrystone 1088 -> 662 (1.64x)
whole suite 473 -> 609 MIPS (1.29x), 526 -> 859 DMIPS (1.63x)
`sys/tlb_hit` is 3-10% slower and `sys/uncached` about 10% slower: nearly
every access in both is a callout, so the compare is pure overhead in front
of work that was going to happen anyway. That is the trade the study
predicted — the callout is the better path for miss-heavy work and it is
untouched. `IRIS_NO_KSEG0_FAST=1` disables the path at emit time for A/B.
Docs: docs/jit-inline-memory.md §6a, FEATURES.md, CHANGELOG.md. Tests:
`kseg0_direct_rule` against `translate_impl` over a VA grid, the model K0
table, and the mirror's tracking of Status/Config.
Credit: Jupiter Belic and irix7/iris — the KSEG0 direct-address recommendation
and the measurement method (interleaved `iris-bench` runs reported as ns per
work unit) come from clanker/perf/mem-and-fpu-hot-paths.md,
https://github.com/irix7/iris/blob/main/clanker/perf/mem-and-fpu-hot-paths.md
Owner
|
Yeah, not sure about this. We need user space benchmarks. This adds a check in hot path for memory access which will slow down every TLB mapped transaction which are majority on a working system. |
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.
What this is
KSEG0 is unmapped:
translate_32bit_implandtranslate_64bit_implcompute its physical address asvirt_addr & 0x1FFF_FFFF, with no TLB entry, ASID, generation or alias to consult. The jitv2 inline load/store guard still probed the nutlb for it on every kernel load and store — an index computation, two dependent loads and the validity-bitmask load — which a memory hot-path study measured as the bulk of a 15.8 ns L1 load.The guard now tests one thing first:
0xFFFF_FFFF_8800_0000 >> 29is0x7_FFFF_FFFC— bits 63:32 all ones and bits 31:29 =0b100— so one compare covers both halves of the address test. Emitted on x86-64 that ismovq 0x2520(%rbx),%rcx; shrq $0x1d,%rax; cmpq %rcx,%rax; jz.Why it is exact
jit_kseg0_ruleis derived state. Compiled code reads it throughoffset_of!; only Rust writes it, from the two registers it depends on, at the hooks those registers already own:Status— every change goes throughresync_privilege_state, whose own doc comment states that invariant (MTC0 Status viastatus_changed_cb, all exception-delivery paths,ERET, soft reset, snapshot/digest restore, gdb). That function now refreshes the rule.Config—MTC0 Configis the only writer, andhandle_cp0_side_effectsis the single hook it runs through (the same hook the K0 nutlb flush already relies on). It now refreshes the rule.kseg0_direct_ruleis the executable specification, and it is a literal transcription of what the translator does plus the cacheability check the guard owes:get_privilege_mode() == Kernel— EXL/ERL force it, elseKSU == 00b100translate_64bit_impl's xkseg compatibility arm; a zero-extended0x0000_0000_8xxx_xxxxis xuseg there and TLB-mapped, so accepting it would be a wrong answer, not a slow oneConfig.K0selects a cached algorithm in this model's tableWhen either fails, the rule is
1 << 63, whichvirt_addr >> 29cannot produce (the shift result is at most 35 bits), so every access in that state takes the nutlb probe exactly as before. In 32-bit mode the translator ignores bits 63:32, so the same compare accepts a conservative subset of what it translates — it demands the sign-extended form, whichlui/laproduce — and anything else falls back. Declining is always safe; the nutlb path is untouched.Everything downstream of
phys— L1-D tag, dirty bit, LRU, tcache mapped test, swizzle, store generation bump — is the same emitted code on both arms, so the cache model, VCE, aliasing and the page-generation checks are unchanged. Only howphyswas obtained differs. TLB invalidation is irrelevant to KSEG0 by construction, and the TLB-mapped path is not modified in any way.IRIS_NO_KSEG0_FAST=1disables the path at emit time (likeIRIS_NO_INLINE_MEM), so it can be A/B'd on a live system withj2 flush.What did not work
Worth recording, because it is the reason the change looks the way it does: a predicate evaluated per access (two CP0 loads, a dozen ALU ops) measured worse than the nutlb probe it replaced — an access in the tcache path is only a handful of host instructions. Hoisting the same computation to region entry (
entry_block, once per region) measured worse still, becauseiris-benchenters regions often enough that per-entry work costs more than it saves. One memory-operand compare is the shape that wins.Measurement
Host: Intel i5-8500T, 6 cores, Linux x86_64, load average 3–5 during the runs.
cargo build --release -p iris --features lightning,jitv2, nightly toolchain from the repo's flake. Two binaries (base =ed17f88, fast = this PR), three runs each, interleaved (base, fast, base, fast, base, fast), in-process:ns per work unit, median of three; lower is better. Spread is min–max of the three.
mem/latency_l1(scalar-load chase, 8 KB)mem/copy(load+store stream)mem/fill(store stream)mem/stream_copymem/random(random-page scalar loads)mem/latency_dram(chase)mem/latency_l2mem/unalignedint/dhrystone(string/record traffic)codec/crc32(scalar byte loads)--cache full(where the L2 model, not the guard, dominates): suite 1.02x,mem/latency_l11.70x,mem/random1.22x,mem/copy1.06x,mem/latency_dram1.02x.What it costs
sys/tlb_hitis 3–10% slower (9.27 → 10.15 ns/op) andsys/uncached~10% slower (3.84 → 4.27). Nearly every access in both is a callout, so the compare is pure overhead in front of work that was going to happen anyway. This is the trade the study predicted — the callout is the better path for miss-heavy work, and it is untouched.sys/tlb_hit,sys/uncachedandfpu/scalar_smoved consistently, and the FP one has no mechanism (no memory access in the kernel).Caveats
mem/load_l1/mem/store_l1micro-kernels are not in the checked-in suite, and this rig has no MIPS cross toolchain to add them;mem/latency_l1(scalar loads),mem/fill(scalar stores),mem/copy/mem/stream_copy(load+store streams) andmem/randomstand in for them. The 1.3x scalar-memory bar is met on all of those.sys/tlb_hit,sys/tlb_miss,sys/exception,sys/uncached,sys/llsc), the test battery below, and the--cache full/shadowruns agreeing.Tests
cargo test --workspace— 1351 passed, 0 failed.cargo test -p iris --lib --features lightning,jitv2— 1752 passed, 0 failed (includes the jitv2 equivalence suite).cargo test -p iris --lib --features jitv2,jitv2_lockstep— 1785 passed, 4 failed; all four (every_instr_origin_class_is_tagged_in_traceback,fallback_successor_back_edge_is_still_recorded,lockstep_fpu_trunc_w_s_non_integer_value_sets_inexact,test_draw_debug_counts_every_word_of_a_batch) fail identically onupstream/mainwith the same feature set — verified by stashing this change and re-running them.kseg0_direct_rulechecked againsttranslate_implitself over a grid of VAs spanning xuseg/KSEG0/KSEG1/KSSEG/KSEG3/xkphys, zero- and sign-extended, in both 32- and 64-bit modes (TR_MAPPEDdistinguishes the TLB-mapped arms); the R4400 vs R5000 K0 tables; the mirror trackingStatus(kernel/supervisor/user, EXL, ERL, reserved KSU) andConfigthrough a realMTC0; and a pinned check thatKSEG0_RULE_HIT == 0xFFFF_FFFF_8800_0000 >> 29— the constant is easy to write oneFshort, which makes the fast path silently never fire.cpu-testswas not run: no MIPS toolchain on this host (the flake does not provide one and the cross build needs a from-source binutils/gcc).Docs:
docs/jit-inline-memory.md§6a (design, predicate, measurement),FEATURES.md(the env var),CHANGELOG.md.