perf: icache Phase 3 census — hot/cold layout class CLOSED at 0.92 line utilization

ee_code_census.py --coldbytes: per-block byte-layout census (smc_preamble
via exact structural match of the ProtMode_Manual inline compare, validated
by disassembly; tail_junk after the last unconditional branch; align_pad
gaps) + sample-weighted executed shares + hot-working-set line utilization,
pooling multiple perf.data files. Jitdump overlap dedupe (full resets rewind
the bump allocator; keep the later record).

Session-2 real-gameplay numbers (UYA, 5x10s 20kHz EE-thread bursts):
- SMC compares: 10.26% of STATIC EE bytes (620KB / 2627 blocks) but only
  0.42% of executed samples and 0.41% of hot-set bytes -> L1 stub+table
  redesign dismissed at the >=1%-samples/>=3%-bytes gate.
- tail_junk 0.35% + pad 3.8% of hot-line bytes -> L2/L3 dismissed.
- Line utilization of the 90%-coverage set: 0.915-0.927 -> cross-block
  hot/cold packing PERMANENTLY CLOSED. Compile-order bump allocation is
  already ~92% packed; the entire layout class can recover ~11KB of a
  130KB working set. Only emitted-code density still shrinks it.

pstef adjudication complete: mechanism real (working set >> L1I), remedy
(hot/cold arenas) prizeless here -- out-of-line slowpaths + compile-order
emission get the layout for free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Brian Degenhardt
2026-07-13 15:55:14 -07:00
co-authored by Claude Fable 5
parent 0750f0e8a9
commit faf4346ef4
2 changed files with 391 additions and 4 deletions
+333 -4
View File
@@ -14,8 +14,9 @@ Optionally wrap in `perf record -F 999 -o perf.data` and pass perf.data for a
sample-weighted dynamic mix — samples are binned into block address ranges
directly (no `perf inject` needed).
Usage: ee_code_census.py <jit-XXXX.dump> [perf.data] [--pins s3|s0|none]
Usage: ee_code_census.py <jit-XXXX.dump> [perf.data ...] [--pins s3|s0|none]
ee_code_census.py <jit-XXXX.dump> perf.data --footprint [--window 1.0]
ee_code_census.py <jit-XXXX.dump> perf.data [...] --coldbytes
--footprint (needs perf.data): instead of the category census, measure the hot
JIT working set — bin sample IPs by 64-byte host cacheline over the jitdump
@@ -24,6 +25,16 @@ JIT (EE / mVU0 / mVU1 / ...) and combined. Whole-run numbers overstate the
instantaneous set, so it also reports the per-`--window`-second median. The
number to compare against L1I capacity: A77 64KB, A55/A53 32KB.
--coldbytes (icache Phase 3): per-block byte-layout census + line utilization.
Splits every block into smc_preamble (the ProtMode_Manual inline compare emitted
by memory_protect_recompiled_code — exact structural match) / body / tail_junk
(bytes after the last unconditional branch: vixl literal pools + unreachable),
plus inter-block align_pad gaps. With perf.data (several may be given; samples
are pooled): the executed share of each category, and LINE UTILIZATION — for the
64B lines that make up the hot working set, what fraction of their bytes is
sampled-block body. 1 - utilization is the theoretical ceiling of all packing /
cold-extraction work; the 90%-coverage-set row is the honest headline.
Categories:
ldr_gpr_pinned(reload) loads of a pinned guest GPR into its own pin (seam reloads)
ldr_gpr_pinned(MISSED) loads of a pinned guest GPR into some other reg (should be ~0)
@@ -167,6 +178,317 @@ def classify_block(code, pin_gpr, pins):
return c, per_gpr_ld, n
def _match_smc_preamble(ins):
"""Byte length of the ProtMode_Manual SMC compare preamble at block start.
Matches the exact emission shape of memory_protect_recompiled_code
(iR5900-arm64.cpp): Mov w0,start; Mov w1,nwords; per word {addr-mat x17;
Ldr w8,[x17]; Mov w9,expected; Cmp w8,w9; B.ne(±long form)}; optional
manual_counter epilogue {addr-mat x17; Ldrh w8; Add w8,imm; Strh w8;
Tst w8; B.ne(±long)}. Returns 0 if the block doesn't start with it.
"""
n = len(ins)
pos = 0
def imm_mat(rd): # vixl Mov(w<rd>, imm): movz/movn/orr-imm, + trailing movk*
nonlocal pos
if pos >= n:
return False
i = ins[pos]
ok = (((i & 0xFF800000) in (0x52800000, 0x12800000) and (i & 31) == rd)
or ((i & 0xFF8003E0) == 0x320003E0 and (i & 31) == rd))
if not ok:
return False
pos += 1
while pos < n and (ins[pos] & 0xFF800000) == 0x72800000 and (ins[pos] & 31) == rd:
pos += 1
return True
def addr_mat(rd): # armMoveAddressToReg: adrp+add/orr | movz x + movk x{1..3}
nonlocal pos
if pos >= n:
return False
i = ins[pos]
if (i & 0x9F00001F) == (0x90000000 | rd): # adrp
if pos + 1 < n:
j = ins[pos + 1]
if ((j & 0xFF800000) in (0x91000000, 0xB2000000)
and (j & 31) == rd and ((j >> 5) & 31) == rd):
pos += 2
return True
return False
if (i & 0xFF80001F) == (0xD2800000 | rd): # movz x
pos += 1
while pos < n and (ins[pos] & 0xFF80001F) == (0xF2800000 | rd):
pos += 1
return True
return False
def one(pattern, mask):
nonlocal pos
if pos < n and (ins[pos] & mask) == pattern:
pos += 1
return True
return False
def condbr(): # armEmitCondBranch(ne): b.ne | b.eq +8; b imm26 (long form)
nonlocal pos
if pos >= n:
return False
i = ins[pos]
if (i & 0xFF000010) == 0x54000000:
cond = i & 0xF
if cond == 1: # ne
pos += 1
return True
if cond == 0 and pos + 1 < n and (ins[pos + 1] & 0xFC000000) == 0x14000000:
pos += 2
return True
return False
if not (imm_mat(0) and imm_mat(1)):
return 0
units = 0
while True:
save = pos
if (addr_mat(17) and one(0xB9400228, 0xFFC003FF) # ldr w8,[x17,#i]
and imm_mat(9) and one(0x6B09011F, 0xFFFFFFFF) # cmp w8,w9
and condbr()):
units += 1
continue
pos = save
break
if not units:
return 0
save = pos
if not (addr_mat(17) and one(0x79400228, 0xFFC003FF) # ldrh w8,[x17,#i]
and one(0x11000108, 0xFF8003FF) # add w8,w8,#imm
and one(0x79000228, 0xFFC003FF) # strh w8,[x17,#i]
and one(0x7200011F, 0xFF8003FF) # tst w8,#imm
and condbr()):
pos = save
return pos * 4
def _tail_junk(ins):
"""Bytes after the last unconditional control transfer (B/BR/RET) in the
block = trailing vixl literal-pool data + unreachable bytes."""
last = -1
for k, i in enumerate(ins):
if ((i & 0xFC000000) == 0x14000000 # b imm26
or (i & 0xFFFFFC1F) == 0xD61F0000 # br
or (i & 0xFFFFFC1F) == 0xD65F0000): # ret
last = k
if last < 0:
return 0
return (len(ins) - 1 - last) * 4
def _dedupe_blocks(blocks):
"""Jitdump can contain overlapping records (full rec resets rewind the bump
allocator). Keep the LATER-loaded record for any overlap."""
recs = sorted(((addr, addr + size, idx, b) for idx, b in enumerate(blocks)
for addr, size, _, _ in (b,) if size),
key=lambda r: (r[0], r[2]))
out = []
for lo, hi, idx, b in recs:
while out and out[-1][1] > lo: # overlap with previous kept record
if out[-1][2] < idx:
out.pop() # previous is older — drop it
else:
break # previous is newer — drop this one
else:
out.append((lo, hi, idx, b))
continue
return [b for _, _, _, b in out]
def _iter_samples(perfdata):
"""Yield (time, leaf_ip) from perf.data via perf script — handles both flat
'TIME: IP' lines and call-graph shape (TIME line + IP lines, leaf first)."""
proc = subprocess.Popen(["perf", "script", "-i", perfdata, "-F", "time,ip"],
stdout=subprocess.PIPE, text=True)
pending_t = None
for line in proc.stdout:
parts = line.split()
if not parts:
continue
if parts[0].endswith(":"):
try:
t = float(parts[0][:-1])
except ValueError:
continue
if len(parts) >= 2:
try:
yield t, int(parts[1], 16)
except ValueError:
pass
pending_t = None
else:
pending_t = t
elif pending_t is not None:
try:
yield pending_t, int(parts[0], 16)
except ValueError:
continue
pending_t = None
proc.wait()
def run_coldbytes(blocks, perfdatas):
blocks = _dedupe_blocks(blocks)
meta = [] # (lo, hi, group, smc_bytes, junk_bytes)
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]
smc = _match_smc_preamble(ins) if name.startswith("EE_") else 0
junk = _tail_junk(ins)
meta.append((addr, addr + size, group, smc, junk))
lows = [m[0] for m in meta]
# -- static byte census -------------------------------------------------
stat = {} # group -> [blocks, bytes, smc_bytes, smc_blocks, junk_bytes, pad_bytes]
for k, (lo, hi, g, smc, junk) in enumerate(meta):
s = stat.setdefault(g, [0, 0, 0, 0, 0, 0])
s[0] += 1
s[1] += hi - lo
s[2] += smc
s[3] += smc > 0
s[4] += junk
if k + 1 < len(meta):
gap = meta[k + 1][0] - hi
if 0 < gap <= 64:
s[5] += gap
print("== STATIC byte layout (deduped jitdump) ==")
print(f"{'group':8s} {'blocks':>7s} {'KB':>8s} {'smcKB':>7s} {'smc%':>6s} "
f"{'smcBlks':>7s} {'junkKB':>7s} {'junk%':>6s} {'padKB':>6s} {'pad%':>6s}")
for g in sorted(stat, key=lambda g: -stat[g][1]):
b, tot, smc, sb, junk, pad = stat[g]
kb = lambda v: v / 1024
print(f"{g:8s} {b:7d} {kb(tot):8.1f} {kb(smc):7.1f} {100 * smc / tot:6.2f} "
f"{sb:7d} {kb(junk):7.1f} {100 * junk / tot:6.2f} {kb(pad):6.1f} {100 * pad / tot:6.2f}")
if not perfdatas:
return
# -- sample attribution --------------------------------------------------
def locate(ip):
j = bisect.bisect_right(lows, ip) - 1
if j >= 0 and ip < meta[j][1]:
return j
return -1
csamp = {} # group -> Counter(category)
line_cnt = Counter() # 64B line -> samples (JIT only)
line_grp = {} # 64B line -> group of first sample's block
blk_sampled = set() # indices into meta with >=1 sample
n_tot = n_jit = 0
for pd in perfdatas:
for _t, ip in _iter_samples(pd):
n_tot += 1
j = locate(ip)
if j < 0:
continue
n_jit += 1
lo, hi, g, smc, junk = meta[j]
off = ip - lo
cat = ("smc_preamble" if off < smc
else "tail_junk" if off >= (hi - lo) - junk else "body")
csamp.setdefault(g, Counter())[cat] += 1
csamp.setdefault("ALL-JIT", Counter())[cat] += 1
line = ip >> 6
line_cnt[line] += 1
line_grp.setdefault(line, g)
blk_sampled.add(j)
print(f"\n== EXECUTED share ({n_tot} samples pooled from {len(perfdatas)} file(s), "
f"{n_jit} in JIT) ==")
print(f"{'group':8s} {'samples':>9s} {'smc%':>7s} {'body%':>7s} {'junk%':>7s}")
for g in sorted(csamp, key=lambda g: -sum(csamp[g].values()) if g != "ALL-JIT" else 1):
c = csamp[g]
tot = sum(c.values())
print(f"{g:8s} {tot:9d} {100 * c['smc_preamble'] / tot:7.3f} "
f"{100 * c['body'] / tot:7.3f} {100 * c['tail_junk'] / tot:7.3f}")
# -- line utilization ----------------------------------------------------
# For each hot 64B line, classify its bytes: body of a sampled block
# (useful) vs smc/junk of a sampled block, any byte of an unsampled block,
# or gap/pad. Two universes: all sampled lines, and the hottest lines
# covering 90% of JIT samples (== the working set headline from
# --footprint). 1 - utilization = the packing/cold-extraction ceiling.
def classify_lines(lines):
agg = Counter()
for line in lines:
b0, b1 = line << 6, (line << 6) + 64
j = bisect.bisect_right(lows, b0) - 1
if j < 0:
j = 0
p = b0
while p < b1:
if j >= len(meta) or meta[j][1] <= p:
if j + 1 < len(meta) and meta[j + 1][0] <= p:
j += 1
continue
nxt = meta[j + 1][0] if j + 1 < len(meta) else b1
step = min(b1, nxt) - p
agg["gap"] += step
p += step
continue
lo, hi, g, smc, junk = meta[j]
if p < lo:
step = min(b1, lo) - p
agg["gap"] += step
p += step
continue
step = min(b1, hi) - p
if j not in blk_sampled:
agg["cold_block"] += step
else:
# split the [p, p+step) run across smc/body/junk boundaries
q = p
while q < p + step:
off = q - lo
if off < smc:
lim = min(p + step, lo + smc)
agg["hot_smc"] += lim - q
elif off >= (hi - lo) - junk:
lim = p + step
agg["hot_junk"] += lim - q
else:
lim = min(p + step, hi - junk)
agg["hot_body"] += lim - q
q = lim
p += step
return agg
cov90 = _coverage(line_cnt, fracs=(0.9,))[0.9]
hot90 = [l for l, _ in line_cnt.most_common(cov90)]
print(f"\n== LINE UTILIZATION (64B lines; 'useful' = body bytes of sampled blocks) ==")
print(f"{'universe':22s} {'lines':>7s} {'KB':>7s} {'body%':>7s} {'smc%':>6s} "
f"{'junk%':>6s} {'coldblk%':>8s} {'gap%':>6s}")
for label, lines in (("all sampled lines", list(line_cnt)),
("90%-coverage set", hot90)):
a = classify_lines(lines)
tot = sum(a.values())
if not tot:
continue
print(f"{label:22s} {len(lines):7d} {len(lines) * 64 / 1024:7.1f} "
f"{100 * a['hot_body'] / tot:7.2f} {100 * a['hot_smc'] / tot:6.2f} "
f"{100 * a['hot_junk'] / tot:6.2f} {100 * a['cold_block'] / tot:8.2f} "
f"{100 * a['gap'] / tot:6.2f}")
# per-group utilization over the 90% set (where do the wasted bytes live?)
by_grp = {}
for line in hot90:
by_grp.setdefault(line_grp.get(line, "?"), []).append(line)
print("\n90%-set by group:")
for g in sorted(by_grp, key=lambda g: -len(by_grp[g])):
a = classify_lines(by_grp[g])
tot = sum(a.values())
print(f" {g:8s} {len(by_grp[g]) * 64 / 1024:7.1f}KB body {100 * a['hot_body'] / tot:6.2f}% "
f"smc {100 * a['hot_smc'] / tot:5.2f}% junk {100 * a['hot_junk'] / tot:5.2f}% "
f"coldblk {100 * a['cold_block'] / tot:5.2f}% gap {100 * a['gap'] / tot:5.2f}%")
def _coverage(cnt, fracs=(0.5, 0.9, 0.99)):
"""{addr_unit: samples} -> {frac: n_units covering that fraction, hottest-first}."""
tot = sum(cnt.values())
@@ -283,10 +605,13 @@ def run_footprint(blocks, perfdata, window):
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("jitdump")
ap.add_argument("perfdata", nargs="?", help="perf.data for sample weighting")
ap.add_argument("perfdata", nargs="*", help="perf.data for sample weighting "
"(several allowed; pooled in --coldbytes, first used elsewhere)")
ap.add_argument("--pins", choices=sorted(PIN_MAPS), default="s5")
ap.add_argument("--footprint", action="store_true",
help="hot-working-set report (needs perf.data); skips the census")
ap.add_argument("--coldbytes", action="store_true",
help="byte-layout census + line utilization (icache Phase 3)")
ap.add_argument("--window", type=float, default=1.0,
help="footprint time-window seconds (default 1.0)")
args = ap.parse_args()
@@ -294,7 +619,11 @@ def main():
if args.footprint:
if not args.perfdata:
sys.exit("--footprint requires perf.data")
run_footprint(list(parse_jitdump(args.jitdump)), args.perfdata, args.window)
run_footprint(list(parse_jitdump(args.jitdump)), args.perfdata[0], args.window)
return
if args.coldbytes:
run_coldbytes(list(parse_jitdump(args.jitdump)), args.perfdata)
return
pin_gpr = PIN_MAPS[args.pins]
@@ -326,7 +655,7 @@ def main():
print(f" {GPR_NAMES[g]:5s} {'PIN' if g in pin_gpr else ' '} {v:7d}")
if args.perfdata:
out = subprocess.run(["perf", "script", "-i", args.perfdata, "-F", "ip"],
out = subprocess.run(["perf", "script", "-i", args.perfdata[0], "-F", "ip"],
capture_output=True, text=True)
ips = [int(l.strip(), 16) for l in out.stdout.splitlines() if l.strip()]
ranges = sorted(per_block)
+58
View File
@@ -164,6 +164,64 @@ SotC's entire boot storm emitted 4.7MB). Block discards: 0 in both.
must quote its resolution ceiling; sweep window sizes — scaling with window
size means under-resolution. 20kHz single-thread attach is the honest tool.
## Phase 3 — cold-byte census + line utilization (2026-07-10): LAYOUT CLASS CLOSED
Design plan: streamed-soaring-hennessy (Phase-3 rewrite). Tool:
`ee_code_census.py --coldbytes` — splits every jitdump block into
`smc_preamble` (exact structural match of the ProtMode_Manual inline compare,
validated by disassembly) / `body` / `tail_junk` (bytes after the last
unconditional branch = vixl literal pools) plus inter-block `align_pad`, then
sample-weights with the session-2 20kHz EE-thread bursts and computes **line
utilization**: what fraction of the hot working set's 64B-line bytes is
executed block body.
### Static byte layout (UYA session-2 jitdump, deduped)
| group | blocks | KB | smc% | junk% | pad% |
|---|---|---|---|---|---|
| EE | 38,043 | 6041 | **10.26** (620KB, 2627 blocks) | 0.55 | 4.03 |
| IOP | 7,947 | 1398 | — | 2.39 | 0.99 |
| VU1/VIF/VU0 | 1,126 | 1845 | — | ~0 | ~0.1 |
### Executed share + line utilization (5×10s 20kHz bursts pooled; gameplay-only burst in parens)
| metric | value |
|---|---|
| smc_preamble, % of EE-block samples | **0.421%** (0.30%) |
| smc_preamble, % of ALL-JIT samples | 0.257% (0.18%) |
| tail_junk samples | 0.000% |
| **line utilization, 90%-coverage set** | **92.7%** (91.5%) — body 92.7 / smc 0.4 / junk 0.4 / cold-block 2.8 / gap 3.7 |
| line utilization, all sampled lines | 89.7% (89.7%) |
### Gate resolutions (all four steps, one census)
| lever | gate | measured | verdict |
|---|---|---|---|
| L1 SMC stub+table redesign | ≥1% samples or ≥3% hot bytes | 0.42% / 0.41% | **DISMISSED-with-number.** The static 620KB of compare code is real but almost entirely in cold blocks — the hot set barely executes it |
| L2 literal/pad packing | ≥5% of hot-line bytes | junk+gap ≈ 4.1% | **DISMISSED.** And most "gap" is 16B block alignment, which buys fetch alignment |
| L3 cold-tail extraction | never-sampled runs inside hot blocks | hot blocks are >91% body | **DISMISSED.** Nothing to extract — exits/slowpaths were already out-of-line |
| L4 cross-block packing (arena) | utilization <0.7 reopens, ≥0.8 closes | **0.9150.927** | **PERMANENTLY CLOSED** |
### The Phase-3 verdict
**Compile-order bump allocation already produces a ~92%-packed hot working
set.** The theoretical maximum ANY layout work (hot/cold split, arenas,
cold-extraction, padding elimination — the entire class) could recover is
~8% of the working set ≈ 11KB of 130KB — nowhere near the 66127KB L1I
overshoot. The working set is irreducible by layout: it is executed body
bytes. What can still shrink it: **emitted-code density** (fewer bytes per
hot block — the EE-SRA class of work) and nothing else. The live frontend
levers on SD865 therefore remain density + the RSB/P2-2 mispredict line
(1.51.9/kinsn in real play); on RK3562 density only.
This also finishes the pstef adjudication with an unexpected twist: his
mechanism holds (working set ≫ L1I), but his structural remedy (hot/cold
arenas) has no prize here — a JIT that keeps slowpaths out-of-line and emits
in compile order gets the layout for free.
Repro: `ee_code_census.py <jit.dump> <hf-*.perf.data ...> --coldbytes`
(session-2 raw data: scratchpad `icache/session2/`).
## Caveats
- Footprint measured on the M2 (identical emitters/layout logic; host ≠ device),