3.5 KiB
§208 — TWO NAMED LOCALS FOR ONE RELOADED EXPRESSION BUY TWO ALLOCNOS — the naming granularity owns the REGISTER SPLIT, not just the load count (P31 S58)
Bounds §193-E / §193-H, which own how MANY lws a pointer-derived base emits (one per CSE-live
interval / per store-separated run) and name the C local as the only lever over that count. Neither
says what happens when you declare two locals for the same expression. Refutes §137's
"source-level levers are a dead end for REGALLOC-PERM" for micro-functions.
THE TELL. The target reloads the same expression twice into two different registers:
lw $v1, 0x20($s0) <- reload #1
sh $v0, 0x12($v1)
lw $v0, 0x20($s0) <- reload #2, DIFFERENT register
sh $zero, 0x14($v0)
(✔ byte-checked: asm/ov_SC03_028/.../func_80185028.s idx 5/9 — $v1 then $v0, same 0x20($s0).)
THE LAW. One named local for both reads gives gcc one pseudo; local-alloc coalesces the two reload sites onto one hard register and you get an unfixable clean swap. Two separate locals mint two pseudos ⇒ two allocnos ⇒ two hard registers, restoring the split. The edit is one extra declaration and it changes the pseudo set, not the schedule — which is precisely why §137's two-compile priority arithmetic cannot see it.
| function | binary | residual before | edit | result |
|---|---|---|---|---|
func_80185028 |
ov_SC03_028 | single pointer CSE'd → lost the $v1/$v0 split and the two zero-stores' delay-slot pairing |
two distinct locals for the two reloads of *(a0+0x20) |
MATCH 29 |
func_801801AC |
ov_SC04_004 | REGALLOC-PERM $v1>$a0>$v1 |
p1/p2 as separate locals (target keeps $a0 for the first — pointer and saved halfword — and $v1 for the reload) |
MATCH 34 |
func_801852C8 |
ov_SC03_006 | 4 tail diffs; one reassigned local let gcc merge both lw 0x20($a0) into one long-lived pseudo |
a second pointer local born only after the halfword dies | MATCH 15 |
func_801A7F84 |
md_SC07_004 | $v1/$a0 split between two identical inline reload expressions |
pin ONE side (register s32 v1 __asm__("$3")) and leave the |= block's temp unpinned |
MATCH 52 |
THE READ-BACK RULE. A target that reloads one expression into two different registers is telling
you the source had TWO locals, not one reassigned local. func_801852C8's note puts it best: the
second lw landing in the halfword's freed register is the diagnostic, and it is in the target
bytes, not in your diff.
BOUND 1 — it only fires when the two reads are separated by a store or a call. With nothing
between them cse merges the two locals back into one and the extra declaration is inert.
BOUND 2 — keeping the FIRST value live across the separator undoes it. func_801852C8
explicitly: a second pin on the first pointer re-merged the loads and cancelled the whole lever.
THE OPPOSITE DIRECTION, for completeness. Where the residual is a value staying live too long
rather than a register split, sink the load INTO the if condition: func_80182960
(ov_SC04_002) hoisted *(p+0xC4) into a local first and gcc kept it live across the store, used li
instead of ori for 0x8A10, reordered sh 0x52 past the andi and duplicated the epilogue — 31
ins. Written inline in the condition: MATCH.
PROVENANCE. Four independent wave-ac/ad/ae/af cards, four different overlays. func_80185028's
two-register reload shape was re-read out of the .s this session.