5.3 KiB
§311 — A JOIN-BLOCK STORE CAN SWALLOW AN ARM'S LOAD-DELAY nop: WHEN ONE ARM'S VALUE COMES FROM A LOAD, DUPLICATE THE STORE INTO BOTH ARMS (P31 S64 t5j-t5m; byte-proven func_8017D7E0)
(THE THIRD POLE of the duplicate-vs-join store dial, and the only one whose mechanism is the LOAD-DELAY HAZARD. Bounds §165-21, whose "two computed arms want one shared trailing store; §164-73/§164-74's write-the-store-twice is scoped to CONSTANT arms" reads as a two-way split — this is a MIXED pair (one literal, one loaded value) where the duplicate wins. Extends §164-73 / §164-74 (same prescription, two literals, jump.c:728-760 collapse + regalloc mechanism) and §167-26 (fall-through case, zero length cost, saved-register order only). Same tell as §176-A2, different lever: there the −1 nop is bought back by moving the store above a call; here no statement move reaches it, because the store's position is pinned by the if/else join.)
THE TELL. LENGTH-DRIFT/-1 — exactly one instruction short — with every instruction past the if/else join read one slot early, and the target holding a bare nop your draft does not have, sitting between an arm's final load and the store:
target 143: 84220000 lh $v0,0($at) <- the else arm's last insn
target 144: 00000000 nop <- YOUR DRAFT DOES NOT HAVE THIS
target 145: ae220014 sw $v0,0x14($s1) <- the store, ONE copy
target 146: 02202021 move $a0,$s1 <- the next call's arg setup
…
target 163: 27bd0038 addiu $sp,$sp,0x38 (you read `jr $ra` here — one line early)
Residual quotes it as [144, "02202021 move a0,s1", "00000000 nop"] … [163, "03e00008 jr ra", "27bd0038 addiu sp,sp,0x38"] — a pure one-slot displacement, not a permutation.
THE LAW. maspsx emits the load-delay nop only where the final stream still carries the hazard (§176's "the LOAD-delay nop is UNCONDITIONAL — do not write it yourself"), and sched1's scope is the basic block (pass order §45-B; the same scope fact §164-69 turns into a register lever). Compute the value into a temp and store it once after the join and the store lives in the JOIN block, whose other members — the following call's move/lui argument setup — are independent of the arm's load and legally schedulable between it and the store. One of them covers the hazard, maspsx emits no nop, and you are one instruction short with the whole tail displaced. Write the store inside each arm and it sits in that arm's own block, data-dependent on that arm's load, with nothing independent left to interleave — the nop survives at the right byte. jump2's cross_jump then merges the two identical sw tails back into ONE store at the join (§50-B / §162h floor met), so the duplicate is byte-free: the emitted object holds a single sw $v0,0x14($s1), exactly as the target does.
THE C SHAPE. Broken — the temp moves the store out of the arm's block:
s32 val;
if (cond) { val = 0x4B0; } else { val = TBL[*(u8 *)(p + 5)]; }
*(s32 *)(p + 0x14) = val;
Fixed — write the store twice; cross_jump refunds it:
if (cond) { *(s32 *)(p + 0x14) = 0x4B0; }
else { *(s32 *)(p + 0x14) = TBL[*(u8 *)(p + 5)]; }
SCOPE — this is NOT "never a temp". §165-21 and §167-26 both byte-proved a temp (and a ternary) MATCHing on their shapes; the dial has three poles and the discriminator is what the arms hold:
- two literals ⇒ §164-73 / §164-74 — duplicate; tell is a literal in the branch delay slot + a later load transposed above the store.
- two single-insn expressions over the same live registers ⇒ §165-21 — one shared store; duplicating costs +2.
- one literal + one value ending in a LOAD, with a call immediately below the join ⇒ this entry — duplicate; tell is
LENGTH-DRIFT −1and a missingnop, not a register permutation.
BYTE EVIDENCE. func_8017D7E0 (ov_SC06_033, 166 ins, banked at src/ov_SC06_033/ov_SC06_033_jr_8017C24C.c:3568-3572). v3 (pointer-CSE, val temp present): closeness 22 / nins 165 / LENGTH-DRIFT/-1? at 127, explains: partial. v4a/b/c reordered the tail statements around the temp — inert, still 22, which is what rules out §176-A2's statement-move lever. v5 (temp deleted, store duplicated into both arms): closeness 2, nins 165 → 166, the LENGTH-DRIFT class gone entirely, leaving only an unrelated 2-insn operand-order residual at [127,128]. The final 2 → 0 came one step later from an ordinary source-order edit (D_801274EA before D_801274EC, inlining a (u16) cast condition) — §176-A / §165-14 territory, not part of this law. Re-read at vet time out of the banked object: objdump of the banked build shows nop at 0x240 and exactly one sw $v0,0x14($s1) at 0x244 — the merge is visible in the bytes, and 0x240/4 = 144 is the residual's own index.
⚠ MECHANISM BOUND (R14). No -dS/-dR dump was taken. The step "an independent join-block insn covers the cross-block hazard" is read off the final stream plus the pass order, not off the scheduler; -dS on the two spellings is the cheap confirmation and has not been run. What is measured is: the temp spelling is −1 with the nop absent, the duplicated spelling restores it at the correct byte, and the object still emits only one store.