[iterate-4C] perf: cache running-thread ctx ptr across superblock chain

run_superblock re-resolved the running thread's PpcContext on every
chained block via two bounds-checked slot lookups (ctx_mut_ref for the
step + ctx(hw_id) for next_pc) — ~20M double-lookups per 100M-instr
window. The running thread is FIXED for the whole chain (an import
thunk or any sync-sensitive/MMIO op breaks the chain before any kernel
mutation could restructure the runqueue; step_block is pure guest
interpretation; the probe closure is read-only), so its context heap
slot is stable. Resolve the raw *mut PpcContext once before the loop
and reuse it — same raw-pointer discipline as block_ptr.

~3% faster clean wall (1.91s -> 1.85s, n=100M, best of 5). Golden
sylpheed_n200m BYTE-IDENTICAL under interp, XENIA_JIT=1, and
XENIA_JIT=1+XENIA_JIT_REGCACHE=1.

Found via exclusive-attribution profiling: the run is overhead-bound
with the non-step cost spread thin across the per-block chaining loop
(no single dominant lever); step_block itself is ~46% and only the JIT
attacks it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-04 21:18:18 +02:00
parent 1ffa3fb56d
commit 7ec6941fe8

View File

@@ -3159,6 +3159,20 @@ fn run_superblock(
let mut pc_before = first_pc_before;
let mut total_executed: u64 = 0;
// PERF: the running thread (`thread_ref`) is FIXED for the entire
// superblock chain — no thread spawn/exit/migration happens between
// iterations. `step_block` is pure guest interpretation; an import thunk
// or any sync-sensitive/MMIO op BREAKS the chain (below) before any kernel
// mutation could restructure the runqueue; `fire_block_entry_probes` is
// read-only; `block_cache.lookup_or_build` touches only `wc`. So this
// thread's `PpcContext` heap slot is stable for the whole loop and we can
// resolve it ONCE here — instead of a bounds-checked double slot lookup
// (`ctx_mut_ref` + `ctx`) on EVERY chained block (~10M/round). Same
// raw-pointer discipline as `block_ptr`; and `ctx(hw_id) ==
// ctx_mut_ref(thread_ref)` throughout because `running_idx` is unchanged.
// Byte-identical.
let ctx_ptr: *mut xenia_cpu::PpcContext = kernel.scheduler.ctx_mut_ref(thread_ref);
let (result, last_block_ptr, last_pc_before) = loop {
let mmio_before = mem.mmio_access_count();
let block = unsafe { &*block_ptr };
@@ -3167,7 +3181,7 @@ fn run_superblock(
// `ctx_mut_ref` slot lookups — for cycle-before, the step, and
// cycle-after — each a double bounds-checked index). Byte-identical.
let (result, executed) = {
let ctx = kernel.scheduler.ctx_mut_ref(thread_ref);
let ctx = unsafe { &mut *ctx_ptr };
let cycle_before = ctx.cycle_count;
// JIT seam (XENIA_JIT): run the JIT-compiled block if enabled, else
// the interpreter. The JIT leaves ctx.cycle_count/pc and
@@ -3205,8 +3219,9 @@ fn run_superblock(
// Decide whether the NEXT PC is an ordinary guest block we can
// chain into. Anything else (thunk / halt sentinel / unmapped)
// needs the full prologue dispatch next round.
let next_pc = kernel.scheduler.ctx(wc.hw_id).pc;
// needs the full prologue dispatch next round. `ctx_ptr` aliases the
// running thread's context (stable for the chain — see above).
let next_pc = unsafe { (*ctx_ptr).pc };
if next_pc == LR_HALT
|| (kernel.pc_in_thunk_band(next_pc) && thunk_map.contains_key(&next_pc))
|| !mem.is_mapped(next_pc)