6.7 KiB
§211 — HOIST THE LOOP INIT ABOVE THE DOMINATING GUARD: it fills the guard's delay slot AND flips the counter/pointer register pair (P31 S58)
THE TELL. The guard branch's delay slot holds an induction-variable init:
lbu $v1, 0x10A($a0)
addiu $v0, $zero, 0x1
beq $v1, $v0, .L80184BA4
addiu $a1, $a0, 0xE4 <- the CURSOR init, in the GUARD's slot
addu $v1, $zero, $zero <- the counter, in $v1
(✔ byte-checked: asm/ov_SC04_011/.../func_80184B3C.s idx 0-4.)
THE LAW. An init written INSIDE the guarded block can never be scheduled into the guard's delay slot. Hoisting it above the guard does two things at once: (a) it becomes eligible for that slot, and (b) it lengthens the pseudo's live range across the guard block, flipping the local-alloc density contest between the counter and the pointer.
| function | binary | in-block spelling | hoisted spelling |
|---|---|---|---|
func_80184B3C |
ov_SC04_011 | every variant tried (wp-then-i, i-then-wp, decl-in-block, while vs for) allocated cursor→$v1 / counter→$a1 — the exact swap |
s16 *wp = (s16*)((s32)a0 + 0xE4); before the if ⇒ MATCH 28 |
func_80189EB0 |
ov_SC02_027 | init inside the slow arm: near 7 (li outside the slot + full register swap) |
i = 4 above the +0xDE guard ⇒ addiu $v1,$zero,4 in the beqz slot, $v1=counter / $v0=pointer, MATCH 30 |
func_801861D4 |
ov_SC04_011 | inits swapped: $s0 materialised before $s1 was defined |
i = 0; ptr = ...; as separate statements before the for ⇒ target's sw/lui order |
THE COMPANION NEGATIVE — init PLACEMENT has no default, so A/B both forms. Two cards in the same harvest want opposite answers, and neither is predictable from the shape:
func_8018AA88(ov_SC02_005, 9 ins):for (i = 11, ptr = &D_801E4A84; ...)— comma-init in the header — put counter in$v0and pointer in$v1, reversed vs the target (7/9 mismatched, ADDRESSING class). Hoisting both inits into preceding statements (or equivalentlydo{}while(--i>=0)) gave the target's counter=$v1/pointer=$v0. Local declaration order between the two pseudos was tested both ways and is irrelevant. The hypothesis "the second-defined pseudo claims$v0" was falsified on this card; the determinant is init statement placement.func_801871E4(ov_SC02_005, 39 ins): the opposite —for (v = -1, i = 0; ...)as ONE comma-init is what landsaddiu $a0,$zero,-1+addu $v1,$zero,$zeroadjacently in straight-line code; a separatei = 0;statement gets duplicated by the scheduler into BOTH branch delay slots (move v1,zero×2).
A THIRD PLACEMENT CONSTRAINT — a loop-invariant constant init FLOATS ABOVE an address
materialisation. func_801ACD4C (md_SC07_004): plain source order let sched2 float i = 0 above
the lui/addiu of &D_801F8B18. __asm__ volatile("") over-pinned (it dragged sw $ra below
the setup) and an initializer-list i = 0 did nothing; the fix was to create a real data
dependence on the address between the two statements (first = base[0]; placed before i = 0;).
BOUNDARY. All of the above is local-alloc density and delay-slot eligibility, so it only bites
when the guard/branch actually has a fillable slot and the two induction pseudos actually contend.
On a loop with no dominating guard there is nothing to hoist above.
Addendum (P31 S63 t5b-t5d, func_8017DD80): §211's guard-hoist is a lever for a materialisation, and it INVERTS on a copy. Every card in §211 and in its S58b addendum hoists an insn that computes a fresh value (addiu $a1,$a0,0xE4, addiu $v1,$zero,4, i = 0); when the target's guard slot instead holds a bare addu $rD,$rS,$zero copy of a pseudo defined one statement earlier, hoisting that copy above the guard does not relocate it into the slot — it lands the copy adjacent to its source's own def inside one basic block, where §46-L2/§48-B's EBB rule ("a source-level fp = q; ALWAYS dies unless defined in a guard block and used in the loop") deletes it and folds its destination straight into the producing ALU op. An instruction is eliminated, not moved: LENGTH-DRIFT/-1 with the whole tail shifted one slot early. (Which pass performs the fold was not traced — empirical, not internals-verified; the deletion law it instantiates is byte-proven at §46-L2/§48-B/§162p.)
THE TELL. Target: addu $a1,$a2,$v0 (address calc) ; blez $v1,.L ; slot addu $a0,$a1,$zero. The hoisted draft has no copy at all and the address calc writes the copy's register directly — addu $a0,$a2,$v0 — at nins_mine = nins_tgt - 1. Read the slot insn's SHAPE before applying §211: rD,rS,$zero ⇒ do NOT hoist; a computed operand pair ⇒ §211 applies as written.
THE FIX FOR THIS SHAPE. Leave the copy inside the guarded block and park §164-36a's zero-byte __asm__ __volatile__(""); between the address calc and the guard — reorg.c:675 stop_search_p halts fill_simple_delay_slots' backward scan on any asm, so the addr calc can no longer be stolen into the slot and fill_eager_delay_slots takes the in-block copy instead. The pre-fence residual names itself: the addr calc and the blez appear transposed against the target (mine blez then addu, target addu then blez), which is the backward scan having won.
BYTE EVIDENCE — func_8017DD80 (ov_SC06_025, 55 ins, banked src/ov_SC06_025/ov_SC06_025_jr_8017BEBC.c:3719; target 8017ddc8-8017ddd0). v3 (pins + a $2-pinned integer-space temp for param_1[4]*4, §239) = closeness 2, residual exactly [[18,"blez v1,78","addu $a1,$a2,$v0"],[19,"addu a1,a2,v0","blez $v1,.L8017DDF8"]]. v4/v5 hoisting puVar3 = puVar7; above the if — the §211-prescribed edit — regressed to closeness 39, LENGTH-DRIFT/-1, 54 vs 55. v6/v7 reverted the hoist and added the one fence line ⇒ MATCH, closeness 0, residual []. The fence is solo-proven (§266): it is the only delta between the closeness-2 and the closeness-0 build.
DISCRIMINATE against the two near neighbours before reaching for this. §164-82 has the same surface tell (LENGTH-DRIFT −1, a guard branch whose slot holds addu $sD,$v0,$zero) but its cause is a hoisted LOAD and its fix is the test-then-re-read spelling — that one applies when the slot copy's source is a memory value, this one when it is a just-computed address. And §156's sub = pct bullet prescribes the opposite placement ("survives cse ONLY placed in the LOAD's bb before the branch; in an arm every spelling dies") — that regime has the copy's use two fall-through branches down, so cse's follow-jumps table reset saves it; here the use is in the same block as the def and the guard block is the only surviving boundary.