6.3 KiB
§178 — SIX LEVERS MINED FROM THE WAVE-P JOURNALS (P31 S52), each byte-proven and source-cited
Wave P's repair agents did something the campaign has rarely got: they read cc1 -dS/-da dumps and
then the gcc-2.7.2 source, and four of them refuted the first pass's own diagnosis. Every lever
below took a draft to MATCH; every one names the file and line that explains it. The recurring
meta-finding is stated first because it is worth more than any single lever:
"REGALLOC-PERM" is the most over-diagnosed class in this project. In four separate wave-P functions the visible symptom was a register swap and the actual cause was in
cse.corsched.c, decided before allocation — which is exactly why pins, declaration order and statement order all failed on them. When a pin sweep plateaus, stop pinning and dump the pass.
A. THE $0-ADD OPAQUE COPY defeats make_regs_eqv (func_80033398, 93 ins)
Symptom: srl $s6,$s7,16 where the target has srl $s6,$a0,16 — every pin combination left it
bit-identical. Cause (cse.c:826, make_regs_eqv): when the second pseudo of a copy pair outlives
the first and its live range escapes the cse block, it is head-promoted to qty_first_reg, and
canon_reg rewrites every later use of the parameter pseudo to it. No C spelling of a plain
copy escapes this. The lever:
register s32 zr __asm__("$0");
s7v = arg0 + zr; /* emits `addu $s7,$a0,$zero` — but the RTL is a PLUS, not (set reg reg) */
s6v = arg0 >> 16; /* so the srl still reads the parm pseudo, which dies here */
Because it is a PLUS, make_regs_eqv never merges the quantities; local-alloc's copy suggestion
then hands the parm $a0 and deletes the real copy as a no-op. MATCH on the first compile.
Bonus: with the opaque copy in place, 3 of the draft's 5 register pins became dead weight and were
removed — worth re-minimising pins after any cse-level fix.
B. A return <const> IS A PRIORITY-1 HARD-REG SET THE SCHEDULER PLACES FIRST (func_8001BE30, 92 ins)
Symptom: a clean $v1-for-$v0 swap on throwaway temps; six levers failed. Cause, read from the
-da dumps: (set (reg/i:SI 2 v0) (const_int 0)) — the return 0 — is a hard-reg set with
REG_N_SETS(reg 2) > 1 (the function has six return sites), so birthing_insn_p/adjust_priority
give it no boost; it sits at priority 1 while neighbours outrank it, and gcc-2.7.2's backward
list scheduler picks it last, i.e. emits it FIRST in the block. Hard $v0 is then live across
the temp's whole range at local-alloc time, forcing the temp to $v1. sched2+dbr later move
move $2,$0 into the j delay slot — which is why the SHAPE looked right while the register
stayed wrong.
Lever: delete the hard-reg return set from the block. Replace each in-block return 0; with
goto L_ret0; to one shared L_ret0: return 0; tail. The blocks then contain no set of hard $v0,
the temps take $v0, and dbr steals the shared addu $v0,$zero,$zero back into each delay slot.
C. SINGLE-SET TEMPS GET THE BIRTHING BOOST (func_8001D3FC, 196 ins)
birthing_insn_p (sched.c:2469) boosts an insn only when its destination has exactly one
static set (reg_n_sets[dest]==1, the discriminator at :2490). A three-set temp
(ub = expr; u = ub; ub = ub + w - 1;) gets no boost, so its whole chain is picked late and placed
early. Splitting off a genuinely single-set temp boosts the insn and drags its feeder chain down
with it. Byte-proven that the boost must land on the insn you care about: splitting one step
earlier reverted the schedule.
D. A NARROW TYPE BLOCKS COPY ELISION (func_8001D3FC — new idiom)
Once a temp is single-set, a plain same-mode u = ut copy is deleted by cse/coalescing, and u
becomes ut — which is what produced the "$a0↔$v1 swap" the first agent called irreducible.
Declaring the destination narrower (u16 u;) makes it an SI→HI mode-changing copy that cse
cannot propagate through and the allocator cannot coalesce, so the copy survives at its source
position and the entire register assignment falls into place. One type change, ~20 instructions.
E. THE ZERO-OFFSET ALIAS HOLE (func_80037028, 71 ins)
sched.c:memrefs_conflict_p, PLUS-vs-PLUS branch, falls through to find_symbolic_term(x/y) and
reports no conflict when the two symbols differ — but that path is only reachable for the plain
(plus reg symbol_ref) address form, i.e. a field at offset 0. A field at a non-zero offset is
(plus reg (const (plus symbol N))) and comes back conservative. Consequence: an offset-0 store
silently loses its dependence on later loads and floats to the bottom of the block. If a store at
offset 0 is scheduled wrongly, that is why — and giving the struct a non-zero-offset field to touch
restores the dependence.
F. MEM_IN_STRUCT_P ASYMMETRY IN true_dependence (func_80037144, 124 ins)
sched.c:817 skips a dependence when x is /s with a varying address, non-QImode, while
mem is non-/s at a fixed address. So D_800A463C[k].unk00 (struct, varying) does not
depend on plain scalar D_8007622C stores. Model your externs accordingly: struct-vs-scalar is a
scheduling decision, not cosmetics.
G. TWO MODELLING TRAPS THAT COST THESE AGENTS SWEEPS OF HUNDREDS OF COMPILES
sw $a1, D_80076244($a0)is ONE cc1 insn (symbol + scaled index). Thelui/addu/storetriples you see in the.sare gas-G0macro expansion, not cc1 output. Model at the cc1 level or you will chase a phantom. Only a struct array indexed by the slot produces that form; six parallelextern u8 D_800762xx[]make every access a distinctSYMBOL_REFand change the whole dependence graph.__asm__ __volatile__("" ::: "memory")is a FULL barrier — it clobbers all pseudos, so it sinks address chains below stores and can kill a delay-slot steal. When you only want memory ordering, that is the wrong tool; an empty non-volatile__asm__("")was the one that worked as a pure optimization barrier elsewhere (§B above, and the STORE_FLAG_VALUE fold defeat).
Statement-order sweeps are frequently worthless here and the agents proved it by exhaustion: a 2,240-variant sweep and a 5,040-permutation sweep each moved nothing, because the schedule was fully DAG-determined. When order does not matter, the answer is an alias or a set-count property, not a permutation.