Files
BFM-decomp/cookbook/C0232.md
T

6.7 KiB
Raw Blame History

§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 $v0 and pointer in $v1, reversed vs the target (7/9 mismatched, ADDRESSING class). Hoisting both inits into preceding statements (or equivalently do{}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 lands addiu $a0,$zero,-1 + addu $v1,$zero,$zero adjacently in straight-line code; a separate i = 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.