mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
brmis: re-verification after challenge — retract 3-5x margin, correct funnel bounds
bmd challenged the Step-0 dismissal. Recheck (brmis_recheck.py) found the entry8 window (2 insns) is far narrower than the real r22 skid (regularly >64 insns; 76% of EE-block samples land on non-branch IPs, 0.86% on b.cond) so the sub-block attribution was interpretation, not measurement. Corrected skid-honest funnel bounds: UYA <=21.5% of mispredicts / 0.79% of EE-thread cycles, SotC <=28.0% / 1.29% (absurd ceiling 36%/1.67%). G1 still fails -- UYA at every bound, SotC at the honest bound -- so the RSB stays dismissed, but the margin is honest now. Native ~54% confirmed genuine by function-level symbolization against the deployed binary: spu2Mix, vifTransferLoop<1>, outline-atomics thunks (MTVU/MTGS rings), Gif_Unit::Execute, _cpuEventTest_Shared; plus recClearIOP + mVUlookupProg_VU0 top-10 in UYA. Raw capture data preserved at scratchpad/brmis-2026-07-10/ (untracked). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f9f83a19e6
commit
cc339f93cd
+88
-16
@@ -5,8 +5,12 @@ building the MAMBO-X64-style RAS-paired return cache (software RSB + BL/RET
|
||||
pairing; lrps2's direct-mapped shape). Gate G1 for building it: guest-indirect
|
||||
funnel ≥ ~35% of mispredict samples AND ≥ ~1.5% of EE-thread cycles.
|
||||
|
||||
**Verdict: G1 fails by 3–5× on both axes, in both games. The RSB/RAS return
|
||||
cache is DISMISSED with numbers; the mispredict campaign closes at Step 0.**
|
||||
**Verdict (as corrected by the same-day re-verification below): G1 fails on
|
||||
UYA at every attribution bound; on SotC it fails at the honest bound and is
|
||||
ambiguous only under maximally generous attribution. The RSB/RAS return cache
|
||||
stays DISMISSED unbuilt. The original "fails by 3–5×" margin claim is
|
||||
RETRACTED — sampling skid makes the sub-block attribution too coarse to
|
||||
support it; see "Re-verification" below for the honest bounds.**
|
||||
|
||||
## Capture
|
||||
|
||||
@@ -42,23 +46,80 @@ native / dispatcher regions / JIT blocks (instruction class at IP + entry8).
|
||||
|
||||
Attribution notes: r22 sampling has skid — most block samples land on
|
||||
non-branch IPs downstream of the mispredicting branch; region-level shares
|
||||
are sound, sub-block position is smeared. entry8 conflates the dispatcher-Br
|
||||
edge with taken conditional block exits (the long-form `b.cc +8; B <link>`
|
||||
puts BOTH edge types' targets at block entries), so it is an upper-bound
|
||||
contribution to the funnel, not a measurement of it.
|
||||
are sound, sub-block position is smeared. ⚠️ The re-verification showed the
|
||||
skid regularly exceeds 64 insns, so the "block bodies" and "entry8" rows
|
||||
above are NOT separable — use the corrected bounds in the re-verification
|
||||
section, not these rows.
|
||||
|
||||
## The funnel (what an RSB/RAS return cache could touch)
|
||||
|
||||
⚠️ SUPERSEDED by the re-verification section below. The entry8 window (8B =
|
||||
2 insns) is far narrower than the real r22 skid, so these bounds were
|
||||
overconfident-low:
|
||||
|
||||
| | share of mispredicts | × total pool = EE-thread cycles |
|
||||
|---|---|---|
|
||||
| UYA | 6.1% (dispatchers alone) – 13.0% (+ all entry8) | **0.23–0.48%** |
|
||||
| SotC | 7.5% – 12.6% | **0.35–0.58%** |
|
||||
| UYA | 6.1% (dispatchers alone) – 13.0% (+ all entry8) | ~~0.23–0.48%~~ |
|
||||
| SotC | 7.5% – 12.6% | ~~0.35–0.58%~~ |
|
||||
|
||||
## Re-verification (same day — bmd challenged the result)
|
||||
|
||||
Recheck tool: `tools/perf/brmis_recheck.py` (usage: `brmis_recheck.py
|
||||
<jitdump> <perf.data...>`; the native DSO/function split additionally needs
|
||||
the deployed binary under a `--symfs`-shaped tree — see Repro). Three
|
||||
findings:
|
||||
|
||||
**1. Skid is real and large — sub-block attribution is unreliable.** 76%
|
||||
(UYA) / ~75% (SotC) of EE-block mispredict samples land on NON-branch
|
||||
instructions; only 0.86% land on a b.cond. r22's overflow interrupt records
|
||||
the PC wherever execution parks (the next stall point), tens of insns —
|
||||
sometimes >64 — downstream of the retiring mispredict. So the entry8-vs-body
|
||||
split in the table above is interpretation, not measurement. Any bound built
|
||||
from an 8-byte entry window is wrong.
|
||||
|
||||
**2. Corrected funnel bounds** (nearest-upstream-branch analysis: a sample is
|
||||
*entry-reach* if no branch insn sits between block entry and the IP — the
|
||||
incoming edge is then the only in-block candidate for the retired mispredict):
|
||||
|
||||
| bound | UYA share → cycles | SotC share → cycles |
|
||||
|---|---|---|
|
||||
| floor (dispatcher regions only) | 6.1% → 0.23% | 7.5% → 0.35% |
|
||||
| honest ceiling (+ all EE/IOP entry-reach) | 21.5% → **0.79%** | 28.0% → **1.29%** |
|
||||
| absurd ceiling (+ unattributable deep-straight-line samples) | 28.2% → 1.04% | 36.3% → 1.67% |
|
||||
|
||||
Gates (≥35% share, ≥1.5% cycles): **UYA fails at every bound. SotC fails at
|
||||
the honest ceiling and crosses both gates only at the absurd ceiling** (every
|
||||
unattributable sample assigned to the funnel). Two reasons the truth sits
|
||||
near the floor, not the ceiling: (a) entry-reach shadows include taken AND
|
||||
fall-through-resolved guest-cond edges (the long-form `b.cc +8; B <link>`
|
||||
puts both edge types' continuations at block entries) — and with 110–134
|
||||
immediate branches/kinsn vs 12–16 returns/kinsn, a ~1% cond mispredict rate
|
||||
alone explains most of the total pool; (b) they also include BTB-capacity
|
||||
misses on direct link `B`s (130KB working set) — a density problem, not a
|
||||
prediction problem. Definitive split needs counting, not sampling: JITTELEM
|
||||
dispatch counters (Step 0c) would pin the dispatch rate exactly, and a
|
||||
per-site-Br A/B would measure the shared-`Br` aliasing penalty by
|
||||
construction. Neither run — the honest ceiling still fails G1.
|
||||
|
||||
**3. The native ~54% is genuine and now symbolized** (device binary pulled,
|
||||
function-level): body-spread across the emulator's own C, not a boundary-skid
|
||||
artifact. Top offenders — UYA: `isa_native::spu2Mix` (11% of native),
|
||||
`recClearIOP`, `mVUlookupProg_VU0`, `__aarch64_ldadd4_rel` (outline atomics =
|
||||
MTVU/MTGS rings), `_cpuEventTest_Shared`, `vifTransferLoop<1>`,
|
||||
`iopMemRead32`. SotC: `__aarch64_ldadd4_rel` (#1, 10% of native), `spu2Mix`,
|
||||
`vifTransferLoop<1>`, `Gif_Unit::Execute`, `_cpuEventTest_Shared`,
|
||||
`VU_Thread::*` (MTVU coordination). DSO split: 82% (UYA) / 94% (SotC) of
|
||||
native samples are yaps2-qt's own text; libc 8%/4%; kernel 3%/1%. Oddity
|
||||
worth a look someday: 6% (UYA) of "native" is unregistered JIT code (in the
|
||||
perf map but not the jitdump — likely the VIF dynarec, which doesn't emit
|
||||
jitdump records).
|
||||
|
||||
Even a PERFECT return predictor (100% RSB hit rate, zero added cost) recovers
|
||||
at most ~0.6% of the EE thread — and the realistic prize is smaller (the
|
||||
funnel includes JALR targets and jump tables a return cache can't predict,
|
||||
entry8 is mostly guest-cond, and the RSB adds ~6 insns/call + ~8 insns/return
|
||||
of hot bytes against a 92%-utilized icache working set).
|
||||
at most ~0.8% (UYA) / ~1.3% (SotC) of the EE thread under the honest ceiling
|
||||
— and the RSB-addressable subset (JR-$ra hits only) is strictly smaller: the
|
||||
funnel ceiling also contains JALR/jump-table targets, guest-cond edge
|
||||
shadows, and direct-B BTB misses, while the RSB adds ~6 insns/call +
|
||||
~8 insns/return of hot bytes against a 92%-utilized icache working set.
|
||||
|
||||
## Why MAMBO-X64's result doesn't transfer
|
||||
|
||||
@@ -87,19 +148,30 @@ funnel a rounding-error bucket.
|
||||
|
||||
## Gate ledger
|
||||
|
||||
| Gate | Threshold | Measured | Verdict |
|
||||
| Gate | Threshold | Measured (post-re-verification bounds) | Verdict |
|
||||
|---|---|---|---|
|
||||
| G1 share | ≥35% of mispredict samples | 6–13% (both games) | **FAIL** |
|
||||
| G1 pool | ≥1.5% of EE-thread cycles | 0.23–0.58% | **FAIL** |
|
||||
| G1 share | ≥35% of mispredict samples | UYA ≤28% at absurd ceiling; SotC ≤28% honest, 36% absurd | **FAIL** (SotC ambiguous only at absurd bound) |
|
||||
| G1 pool | ≥1.5% of EE-thread cycles | UYA ≤1.04%; SotC ≤1.29% honest, 1.67% absurd | **FAIL** (same caveat) |
|
||||
| Step 1 (RSB build) | G1 | — | **DISMISSED, not built** |
|
||||
| Step 3 (flat LUT) | G2 ≥1% cycles post-Step-1 | latency item ~0.5% ceiling | shelved |
|
||||
|
||||
If the SotC ambiguity ever needs settling: (1) JITTELEM dispatch-class
|
||||
counters (Step 0c — exact, skid-free; one build + one gameplay evening); if
|
||||
the dispatch rate is high, (2) per-site inline LUT+`Br` A/B (de-aliases the
|
||||
shared `Br`; the r22 delta measures the funnel by construction, and it IS
|
||||
the candidate fix).
|
||||
|
||||
## Repro
|
||||
|
||||
```
|
||||
# device: /storage/pcsx2/gameplay-capture-brmis.sh (scratchpad/icache/), then:
|
||||
python3 tools/perf/ee_code_census.py <session>/jit-*.dump <session>/hf-*.perf.data --brmis
|
||||
# skid-honest bounds + native split (re-verification):
|
||||
python3 tools/perf/brmis_recheck.py <session>/jit-*.dump <session>/hf-*.perf.data
|
||||
# native symbolization: pull the DEPLOYED binary into a symfs tree, then
|
||||
# perf script -i <hf> --symfs <tree> -F ip,sym,dso | grep yaps2-qt | ...
|
||||
# rates:
|
||||
awk '/cycles/{c+=$1} /instructions/{i+=$1} / r22 /{m+=$1} END{print 1000*m/i, 100*m*14/c}' stat-*-br.txt
|
||||
```
|
||||
Data: scratchpad/icache/session3-{uya,sotc}/ (b79df7f1 session dir).
|
||||
Data: `/home/bmd/pcsx2/scratchpad/brmis-2026-07-10/session3-{uya,sotc}/`
|
||||
(durable copy; originals in the b79df7f1 session scratchpad under /tmp).
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Re-verify the brmis attribution: quantify r22 sampling skid and symbolize
|
||||
the native bucket. Challenges tested:
|
||||
1. entry8 (8B) is far narrower than real skid -> funnel underestimated?
|
||||
2. "native 53%" bucket never symbolized -> what is actually in it?
|
||||
Usage: brmis_recheck.py <jitdump> <perf.data...>
|
||||
"""
|
||||
import os, sys, struct, bisect, subprocess
|
||||
from collections import Counter
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from ee_code_census import parse_jitdump, _dedupe_blocks, _iter_samples, _insn_class
|
||||
|
||||
jd, pds = sys.argv[1], sys.argv[2:]
|
||||
blocks = _dedupe_blocks(parse_jitdump(jd))
|
||||
meta = []
|
||||
for addr, size, name, code in sorted(blocks):
|
||||
n = len(code) // 4
|
||||
ins = list(struct.unpack_from(f"<{n}I", code)) if n else []
|
||||
group = name.split("_")[0] if " " not in name else f"[{name}]"
|
||||
meta.append((addr, addr + size, name, group, ins))
|
||||
lows = [m[0] for m in meta]
|
||||
|
||||
def locate(ip):
|
||||
j = bisect.bisect_right(lows, ip) - 1
|
||||
return j if (j >= 0 and ip < meta[j][1]) else -1
|
||||
|
||||
# ---- pass 1: in-block sample geometry ---------------------------------------
|
||||
OFFB = [(0, 2), (2, 4), (4, 8), (8, 16), (16, 32), (32, 64), (64, 128),
|
||||
(128, 1 << 30)] # insn-offset buckets
|
||||
off_h = {} # group -> Counter(bucket)
|
||||
updist = {} # group -> Counter(dist class)
|
||||
n_tot = 0
|
||||
n_native = 0
|
||||
grp_tot = Counter()
|
||||
region_tot = Counter()
|
||||
entry_sweep = {} # group -> Counter(W) for W in bytes
|
||||
SWEEP = (8, 16, 32, 64, 128, 256)
|
||||
|
||||
for pd in pds:
|
||||
for _t, ip in _iter_samples(pd):
|
||||
n_tot += 1
|
||||
j = locate(ip)
|
||||
if j < 0:
|
||||
n_native += 1
|
||||
continue
|
||||
lo, hi, name, group, ins = meta[j]
|
||||
if group.startswith("["):
|
||||
region_tot[name] += 1
|
||||
continue
|
||||
off = ip - lo
|
||||
k = off // 4
|
||||
grp_tot[group] += 1
|
||||
for a, b in OFFB:
|
||||
if a <= k < b:
|
||||
off_h.setdefault(group, Counter())[(a, b)] += 1
|
||||
break
|
||||
for W in SWEEP:
|
||||
if off < W:
|
||||
entry_sweep.setdefault(group, Counter())[W] += 1
|
||||
# nearest upstream branch insn (address order == execution order in a
|
||||
# straight-line run; a taken upstream branch would not flow here)
|
||||
d = None
|
||||
for back in range(1, min(k, 64) + 1):
|
||||
if _insn_class(ins[k - back]) != "non-branch":
|
||||
d = back
|
||||
break
|
||||
if d is None:
|
||||
cls = f"entry@{min(k,64)}" if k <= 64 else "nobranch>64up"
|
||||
elif d <= 4:
|
||||
cls = "br<=4"
|
||||
elif d <= 16:
|
||||
cls = "br5-16"
|
||||
else:
|
||||
cls = "br17-64"
|
||||
updist.setdefault(group, Counter())[cls] += 1
|
||||
|
||||
print(f"== {n_tot} samples | native {n_native} ({100*n_native/n_tot:.1f}%) ==")
|
||||
for name, v in region_tot.most_common():
|
||||
print(f" region {name}: {v} ({100*v/n_tot:.2f}%)")
|
||||
|
||||
print("\n== in-block insn-offset-from-entry histogram (% of group) ==")
|
||||
hdr = " ".join(f"{a}-{b if b < 1<<29 else ''}".rstrip("-").ljust(7)
|
||||
for a, b in OFFB)
|
||||
print(f"{'group':6s} {'n':>7s} " + hdr)
|
||||
for g in sorted(grp_tot, key=lambda g: -grp_tot[g]):
|
||||
c = off_h.get(g, Counter())
|
||||
t = grp_tot[g]
|
||||
print(f"{g:6s} {t:7d} " + " ".join(
|
||||
f"{100*c.get((a,b),0)/t:6.2f}%" for a, b in OFFB))
|
||||
|
||||
print("\n== nearest-UPSTREAM-branch distance (execution-order candidates for "
|
||||
"the retired mispredict) ==")
|
||||
cats = ["entry@0", "br<=4", "br5-16", "br17-64", "nobranch>64up"]
|
||||
# merge entry@k into one bucket
|
||||
for g in sorted(grp_tot, key=lambda g: -grp_tot[g]):
|
||||
c = updist.get(g, Counter())
|
||||
t = grp_tot[g]
|
||||
entry = sum(v for k, v in c.items() if k.startswith("entry@"))
|
||||
row = {"entry-reach": entry, "br<=4": c.get("br<=4", 0),
|
||||
"br5-16": c.get("br5-16", 0), "br17-64": c.get("br17-64", 0),
|
||||
"nobranch>64up": c.get("nobranch>64up", 0)}
|
||||
print(f"{g:6s} " + " ".join(f"{k}:{100*v/t:.1f}%" for k, v in row.items()))
|
||||
|
||||
print("\n== entry-window sweep: samples within W bytes of a block entry ==")
|
||||
print("(upper bound on incoming-edge shadow: dispatcher Br + link B + taken "
|
||||
"cond edges all land at entries)")
|
||||
print(f"{'group':6s} " + " ".join(f"W={w:<4d}" for w in SWEEP))
|
||||
for g in sorted(grp_tot, key=lambda g: -grp_tot[g]):
|
||||
c = entry_sweep.get(g, Counter())
|
||||
print(f"{g:6s} " + " ".join(f"{100*c.get(w,0)/n_tot:5.2f}%" for w in SWEEP))
|
||||
disp = sum(v for k, v in region_tot.items() if "Dispatcher" in k)
|
||||
print(f"\nfunnel(W) = dispatcher regions ({100*disp/n_tot:.2f}%) + EE/IOP entryW:")
|
||||
for w in SWEEP:
|
||||
ee = entry_sweep.get("EE", Counter()).get(w, 0)
|
||||
iop = entry_sweep.get("IOP", Counter()).get(w, 0)
|
||||
print(f" W={w:<4d} funnel = {100*(disp+ee+iop)/n_tot:5.2f}% of all samples")
|
||||
|
||||
# ---- pass 2: DSO split of the native bucket ---------------------------------
|
||||
print("\n== native-bucket DSO split (perf script ip,dso) ==")
|
||||
dso = Counter()
|
||||
for pd in pds:
|
||||
proc = subprocess.Popen(["perf", "script", "-i", pd, "-F", "ip,dso"],
|
||||
stdout=subprocess.PIPE, text=True)
|
||||
for line in proc.stdout:
|
||||
parts = line.split()
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
try:
|
||||
ip = int(parts[0], 16)
|
||||
except ValueError:
|
||||
continue
|
||||
if locate(ip) >= 0:
|
||||
continue # JIT — already attributed
|
||||
d = parts[1].strip("()")
|
||||
dso[d] += 1
|
||||
tot = sum(dso.values())
|
||||
print(f"native total via dso pass: {tot}")
|
||||
for d, v in dso.most_common(15):
|
||||
print(f" {100*v/tot:6.2f}% {d}")
|
||||
Reference in New Issue
Block a user