mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 21:36:06 -04:00
8ba3b82307
- func_801392FC (182, close=2->MATCH): CROSS-BB COMBINE LAW (a backedge-crossing count is a variable, not a foldable (s16)load; reorg steals the sll into the delay slot) + VOLATILE-FRAME-PARITY (volatile count-load avoids the cse-common -> combine keep-load -> stale-allocno reload slot). cookbook §36. - func_8013A530 (204, close=10->MATCH): the RC-6 'reload knot' WAS the pins; the $0-ADD OPAQUE COPY (iVar7 = fc + zr, zr=$0) assembles to addu $v1,$a1,$zero but doesn't reserve the reg + 3 zero-byte levers. regalloc.md RC-12/RC-13, §36. - func_8013AF20 (185, close=15->MATCH): the 'impossible' const-hoist order was libgpu addPrim's 24-bit BITFIELD store (store_fixed_bit_field emits 0x00ffffff first) + delete the inherited barrier; P_TAG lifted to engine_types.h (byte-neutral). loop.md, RC-7, §36. - banked via the §35 recipe (fix_arity_callers --any-proto -> sig_unify -> harvest_verify -> dedup_propagate --recover). clean fleet check-all 136/136, dedup 1806/0, 0 NON_MATCHING. - SESSION: 7 giants ×134 total (1 R14-free, 2 Opus-solo, 4 Opus->Fable5); fleet 65.48 -> 65.75%. gcc-2.7.2 reference tree extended (expmed.c/mips.md/... added). Giant queue (§G) stored for next session.
341 lines
22 KiB
Markdown
341 lines
22 KiB
Markdown
# gcc-2.7.2 LOOP OPTIMIZATION pass → residual→C-lever catalog (loop.c)
|
||
|
||
Pass-group: `loop.c` (biv/giv detection, `combine_givs`, strength reduction, invariant
|
||
hoisting/preheader, biv elimination, loop reversal). Compiled against the PINNED triple
|
||
(`tools/bin/gcc-2.7.2-psx/cc1 -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker`).
|
||
All claims below were validated on the REAL cc1 this session (experiment files:
|
||
`.run/gccmap/exp/*.c` + `cc.sh`; live byte-anchors via `tools/match_one.py` on
|
||
`func_80150528`, re-verified `MATCH (53 ins)` from `.run/toolkit/func_80150528.c`).
|
||
|
||
## ⚠️ SOURCE-VERSION CAVEAT (affects every agent using tools/reference/gcc-papermario)
|
||
|
||
`tools/reference/gcc-papermario` is **gcc 2.8.1** (`version.c`), NOT 2.7.2. For loop.c the
|
||
differences are BEHAVIORAL and bit us immediately:
|
||
|
||
| decision point | 2.8.1 (papermario tree) | **2.7.2 (our cc1 — authoritative)** |
|
||
|---|---|---|
|
||
| `maybe_eliminate_biv_1` COMPARE-vs-const, giv w/ const add | add must be SYMBOL/LABEL/CONST/pointer-REG | any `CONSTANT_P` add — **enabled** |
|
||
| `maybe_eliminate_biv_1` COMPARE-vs-const, nonconst add | `&& 0` (disabled, "overflow") | **enabled** |
|
||
| `maybe_eliminate_biv_1` COMPARE-vs-invariant-REG/MEM | `&& 0` (disabled) | **enabled** → biv eliminated even with variable bound (proven expA `test_var`: bound scaled `sll $4,n,3` in preheader) |
|
||
| `check_dbra_loop` reversal init | normalizes nonzero init when counting-only | requires **literal `initial_value == const0_rtx`** (proven expB `norev`: `i=1` start blocks reversal) |
|
||
| `combine_givs` order | qsort hook + refined benefit bookkeeping | plain linked-list pair loop (anchor rules below) |
|
||
| giv increment placement | AUTO_INC logic (moot on MIPS) | always inserted **immediately before the biv increment insn** |
|
||
|
||
A vanilla 2.7.2 extraction is at `.run/gccmap/gcc-2.7.2-vanilla-src/` (loop.c, unroll.c,
|
||
sched.c, cse.c, rtl.h, config/mips). **Recommend promoting it to `tools/reference/` —
|
||
line refs below are to that tree.** (2.8.1 refs marked "pm:".)
|
||
|
||
## L0 — Constants, dump access, and how decisions are scored
|
||
|
||
- RTL dumps: add `-da` to the cc1 line; on file input `foo.i` you get `foo.i.loop` etc.
|
||
(`.run/gccmap/exp/cc.sh` builds .s; copy its cc1 line + `-da` for dumps).
|
||
- Pass order: expand → jump → **cse → loop → cse2** → flow → combine → sched → regalloc
|
||
→ sched2 → crossjump. Loop residuals are DECIDED here but surface post-regalloc.
|
||
- `n_non_fixed_regs` = **28** for PS1 (-msoft-float fixes all 32 FP regs; fixed GPRs =
|
||
$0,$at,$k0,$k1,$gp,$sp,$ra; +hi/lo/+1 special non-fixed).
|
||
- Hoist threshold (`scan_loop:532`): `(loop_has_call ? 1 : 2) * (1 + 28)` = **29 (call) /
|
||
58 (no call)**; decays `-= 3` per moved insn (`move_movables`), and `insn_count *= 2`
|
||
for regs already moved out of another loop (`moved_once`).
|
||
- Strength-reduce threshold (`strength_reduce:3241`): `(call?1:2)*(3+28)` = **31 / 62**.
|
||
- `add_cost` = rtx_cost(PLUS) = `COSTS_N_INSNS(1)` = **2** (2.7.2 `cse.c:680`:
|
||
`(N)*4-2`); `copy_cost` = **4**. Benefit unit: 2 ≈ one insn.
|
||
- **Giv worth-while test** (`strength_reduce`): reduce iff
|
||
`lifetime × threshold × (benefit − 2×biv_count) ≥ insn_count`. A bare `reg = biv<<k`
|
||
giv has benefit 2 → **(2−2)=0 → ALWAYS "not worth while" unless it absorbed a
|
||
combined giv's benefit** — this single fact decides most IV-count/elimination outcomes.
|
||
- Dump phrases: `possible biv, reg R, const = K` (increment found) · `giv ... mult M add
|
||
A` (linear fn of biv; `add (reg:N)` vs `add (const…)` distinguishes L2 outcomes) ·
|
||
`giv at X combined with giv at Y` (g2=X merged into anchor g1=Y) · `not worth while, P
|
||
vs I` (P = lifetime×threshold×benefit, I = insn_count) · `biv R eliminated` ·
|
||
`Can reverse loop` / `Reversed loop and added reg_nonneg` · `Insn N: … moved to M`
|
||
(movable hoisted to preheader) · `Cannot eliminate biv`.
|
||
|
||
---
|
||
|
||
## L1 — IV COUNT & SHAPE (combine_givs / basic_induction_var) — **STEERABLE**
|
||
|
||
**Source:** `basic_induction_var` (2.7.2 loop.c:4819), `find_mem_givs` (:4198
|
||
exclusion), `record_giv` (:4341), `combine_givs_p`/`express_from` (:5457/:5419),
|
||
`combine_givs` (:5494), reduction loop in `strength_reduce` (:3670-3960).
|
||
|
||
**Mechanism (byte-validated):**
|
||
1. Every separately-INCREMENTED pointer/index is its own **biv**. **Bivs never merge
|
||
with each other** (only givs combine, and only within one biv's class).
|
||
2. Mem-address givs (`DEST_ADDR`, from `*(T*)(p+k)`) combine iff **identical
|
||
(mult,add)** or `express_from` succeeds — needs anchor g1's add_val to be
|
||
**CONST_INT**, g2.mult % g1.mult == 0, result a legit address with
|
||
`ADDRESS_COST(new) ≤ ADDRESS_COST(orig)` (MIPS `mips_address_cost`: reg=1,
|
||
reg+small16=1, reg+big=2, reg+(sym+off)=3).
|
||
3. **MIPS + -mgas accepts ANY offset and even reg+symbol pre-reload** (gas macro
|
||
world) → the "offset out of 16-bit range splits the IV" hypothesis is **FALSE on
|
||
this target** (proven expD `d2`: +0x9000 offset still combines, emitted as
|
||
`lw $3,36860($4)` macro). IV splits come from bivs or orphaned givs, not range.
|
||
4. Bare `*q` (mult 1, add 0) is **excluded from givs** (`find_mem_givs:4198`) → a
|
||
bare-dereferenced pointer stays a live biv and can't be re-anchored.
|
||
5. A derived pointer consumed only as same-loop mem addresses **dissolves** (its
|
||
DEST_REG giv goes `maybe_dead`, uses fold into DEST_ADDR givs — proven expD `d3`,
|
||
expE e1/e2: cast/conditional/cross-bb loads all still dissolve).
|
||
6. **Anchor choice**: `bl->giv` is prepend-built during the forward scan, and
|
||
`combine_givs` (2.7.2 loop.c:5494) takes g1 from the list head first (pass 0 =
|
||
replaceable g1 only) → **the LAST-emitted DEST_ADDR giv anchors** and all others
|
||
become `anchor+delta` offsets. NB call args are expanded right-to-left, so *the
|
||
first arg's load is emitted last* — that's why banked func_80150528 anchors at
|
||
+0x20 (first call arg) with 0x38/0x3C offsets off it.
|
||
|
||
**C levers (with proofs):**
|
||
- **Target has biv + ONE derived IV (offset cluster)** → single walked base pointer,
|
||
ALL fields as `*(T*)(p+k)` byte-offsets. gcc reduces the combined giv to one IV,
|
||
anchor per rule 6. **Byte-proven ×134: func_80150528 (53 ins), func_8014F2E0 (66
|
||
ins)** (cookbook §30a#2; re-verified `MATCH (53 ins)` this session).
|
||
- **Target has TWO independently-stepped IVs anchored at different bases** → write two
|
||
walked pointers (`p += K; q += K;`) — two bivs, guaranteed, each anchoring its own
|
||
offsets. Proven on the REAL fn: `.run/gccmap/exp/func_80150528_twoptr.c` produces
|
||
exactly the target's 2-IV structure but anchored at MY chosen +0x58 vs target's
|
||
+0x20 (19/53 mismatch = pure anchor/regalloc delta). **Use the target's anchor
|
||
offset as q's base.**
|
||
- **Do NOT bare-deref a walked pointer you also offset-access** — `*q` keeps biv q
|
||
alive *in addition to* the combined giv IV → 3 IVs (proven
|
||
`.run/gccmap/exp/func_80150528_anchor20.c`: 57 ins, extra `addiu s1,s0,88`). Write
|
||
the zero-offset field as `*(s32*)q` only if the target shows the biv itself doing
|
||
the load; otherwise give the anchor a nonzero base so every access has an offset.
|
||
- Conflicts with the §30 anon-struct `/s` lever (separate pointer re-splits) — read
|
||
the target's IV structure first, pick per §30a#2 NB.
|
||
|
||
**Tells:** parallel `addiu rA,rA,K` + `addiu rB,rB,K` with one endtest = 2 bivs ·
|
||
preheader `addiu rIV, rBASE, A` = combined-giv init, A = anchor offset · offset
|
||
cluster `k(rIV)` all relative to one anchor = combine_givs merged them · in dump:
|
||
`add (const_int k)` givs + "combined with" lines.
|
||
|
||
---
|
||
|
||
## L2 — INDEX-BIV ELIMINATION / ENDTEST REWRITE (maybe_eliminate_biv) — **STEERABLE**
|
||
|
||
**Source:** eligibility gate `strength_reduce` (2.7.2:3688-3720: biv dies in-loop OR
|
||
final value computable), `maybe_eliminate_biv_1` (2.7.2:6004; COMPARE cases from
|
||
:6110 — all paths ENABLED in 2.7.2, see caveat table), elimination executed only when
|
||
`all_reduced == 1` (2.7.2:~3880).
|
||
|
||
**Mechanism:** if the index biv `i`'s only non-giv use is the endtest, the endtest is
|
||
rewritten onto a reduced giv: `i < ARG` becomes `giv_reg < ARG*mult + add`, computed:
|
||
- all-constant → folded immediate (`slt $2,$3,200` — expA `test_const`, 25×8);
|
||
- variable/invariant-reg ARG → `emit_iv_add_mult(ARG,mult,add)` at
|
||
**`where = threshold ≥ insn_count ? preheader : test-site`** (2.7.2:5968) → small
|
||
loops (≤31 insns w/ call, ≤62 without): preheader `sll $4,n,3` (expA `test_var`);
|
||
bigger loops: computed at the bottom, inside the loop.
|
||
|
||
**When elimination FAILS (i kept → index + pointer, 2 IVs):**
|
||
- `i` materialized anywhere else: call arg, stored, returned, compared non-linearly
|
||
(`maybe_eliminate_biv_1` REG case returns 0) — expC `c1`;
|
||
- **`all_reduced == 0` because ANY giv of the class was benefit-ignored** — the classic
|
||
shape: when the array base lives in a hoisted register (`la rB,A` movable — happens
|
||
when the loaded value feeds a call, or in big loops), the address givs have
|
||
`add (reg:N)` (not const), they combine amongst THEMSELVES by identity, and the
|
||
orphaned `i<<k` giv (benefit 0) is "not worth while" → biv kept. Proven expG
|
||
g1/g2, expH h1 (dump: `giv of insn 27 not worth while, 0 vs 10`);
|
||
- giv `!always_computable` (DEST_REG giv set behind a branch), mode mismatch,
|
||
`maybe_dead` anchor.
|
||
|
||
**C levers:**
|
||
- **Target = single scaled-index IV, `slt rIV, #K` endtest, `sym+off(rIV)` HI/LO-reloc
|
||
loads** → plain `for (i = 0; i < N; i++) …arr[i].f…` with global array, value used
|
||
locally (no call consuming the load directly). cc1-proven expA.
|
||
- **Target = pointer-walk with `sltu` vs hoisted end** → pointer form `for (p = arr;
|
||
p < arr+K; p += S)` (biv = pointer; L1 rules for fields). Byte-proven func_80150528.
|
||
- **Target keeps `i` (slt rI, rN) + separate address IV** → make `i` load-bearing the
|
||
way the target shows (pass it, store it, return it) or feed the load to a call
|
||
(expG g1 shape).
|
||
- **Preheader vs in-loop bound scaling** (`sll` before the loop vs at its bottom)
|
||
follows the `where` flip — check loop real-insn count vs 31/62 before assuming a
|
||
scheduling residual.
|
||
|
||
**Tells:** `slt rX, rIV, #imm` where imm = trip×stride = eliminated biv · preheader
|
||
`sll rT, rN, k` = eliminated biv w/ variable bound · `lw rV, sym+off(rIV)` (HI16/LO16
|
||
on a loop load) = index anchored at 0, symbol folded into address · biv init 0 +
|
||
compare vs scaled bound (IV counts BYTES not elements).
|
||
|
||
---
|
||
|
||
## L3 — LOOP REVERSAL → COUNT-DOWN + `bgez` (check_dbra_loop) — **STEERABLE**
|
||
|
||
**Source:** `check_dbra_loop` (2.7.2:5655; reversal branch :5750-5900).
|
||
|
||
**2.7.2 conditions (ALL required):**
|
||
- endtest `LT` vs CONST_INT; increment const; `bound % inc == 0`;
|
||
- `initial_value == const0_rtx` **literally** (start at 0 — no normalization in 2.7.2);
|
||
- counting-only biv (`giv_count==0` and no insn other than its increment/endtest
|
||
mentions it) OR the tiny-loop identity (`giv_count+biv_count+num_mem_sets+
|
||
num_movables+2 == insn_count`, single biv class);
|
||
- `num_mem_sets ≤ 1`, and a single store's address must **vary** (a store to an
|
||
invariant address blocks reversal); `num_nonfixed_reads ≤ 1`; no call; no volatile;
|
||
single fall-through exit.
|
||
|
||
**Result:** `li rI, N-inc` … `addiu rI,rI,-inc; bgez rI,top` (REG_NONNEG note). If a
|
||
(varying-address) store is present, the address giv is re-derived DOWNWARD → target
|
||
shows a down-walking pointer + separate down-counter = **2 IVs** (a reversed loop
|
||
cannot fold the counter into the pointer since the test is `>= 0`).
|
||
|
||
**Proofs (cc1, expB/expC):** `rev` (0-start, unused counter) → reversed w/ `bgez`;
|
||
`norev` (1-start) → NOT reversed; `used` (counter accumulated) → NOT reversed;
|
||
`c2` (`dst[i]=inv` const bound) → reversed, down-walking `addu $2,$2,-4` +
|
||
`addu $3,$3,-1; bgez`.
|
||
|
||
**C levers:** target shows `addiu -1 / bgez` down-count where logic is an up-count →
|
||
write the natural `for (i = 0; i < N; i++)` with the counter otherwise UNUSED and let
|
||
gcc reverse it (do NOT hand-write the reversed loop — initial/final-value insns
|
||
differ). Target shows an up-count `slt` where your draft reverses → violate one
|
||
condition per the target's dataflow: nonzero start, use the counter, second read, etc.
|
||
|
||
**Tells:** `bgez` loop branch + `li N-1` init = reversed counting loop · down-walking
|
||
store pointer + separate down-counter = reversed store loop (c2 shape) · REG_NONNEG
|
||
only arises here (grep dump for `Reversed loop`).
|
||
|
||
---
|
||
|
||
## L4 — INVARIANT HOISTING / PREHEADER COPIES (scan_loop/move_movables) — **STEERABLE**
|
||
|
||
**Source:** candidate filter `scan_loop` (2.7.2:640-720, trap check :716), single-use
|
||
substitution (:735-770), `combine_movables` (:1239), desirability + emission
|
||
`move_movables` (2.7.2:1529), `invariant_p` (:2708, store check :2779),
|
||
`prescan_loop` (:2158, call→unknown_address_altered :2201).
|
||
|
||
**Mechanism:**
|
||
- Movable = single-set pseudo (or invariant consec-sets), src `invariant_p`, safety:
|
||
used only in its bb, OR non-user-var, OR set guaranteed before any use
|
||
(`maybe_never`); `may_trap_p` src can't move past a conditional/call. Moved iff
|
||
`threshold × savings × lifetime ≥ insn_count` (thresholds L0 — for typical loops
|
||
this is nearly always true; the interesting blockers are the SAFETY conditions).
|
||
- **A CALL anywhere in the loop sets `unknown_address_altered`** (`prescan_loop:2201`)
|
||
→ `invariant_p(MEM) == 0` for every load → **no memory load is EVER hoisted from a
|
||
loop containing a call** (proven expC c3: `lw D_SRC` stays in-loop). Don't fight
|
||
it with cached locals — match the target's in-loop reloads by NOT caching.
|
||
- No-call loops: an invariant global load hoists if no `true_dependence` with any
|
||
`loop_store_mems` entry (`invariant_p:2695`; `/s`/`MEM_IN_STRUCT_P` enters here —
|
||
expr.c agent's domain, cookbook §30/§30a#1) — proven expC c2 (`lw D_SRC` hoisted
|
||
over the `dst[i]` stores).
|
||
- **Single-use invariant in a CALL loop is SUBSTITUTED into its use and the set
|
||
deleted** (`scan_loop:735`, only when `loop_has_call`) → the classic
|
||
"`la $a0,SYM` INSIDE the loop at the call-arg site" (proven expC c1). To keep such
|
||
a value hoisted like a target shows, it must be used ≥2× per iteration (or the loop
|
||
must have no call). Conversely an in-loop `la` before a `jal` is THIS, not a missed
|
||
hoist.
|
||
- Two movables loading the SAME value merge to one reg (`combine_movables`,
|
||
`rtx_equal_for_loop_p`; not merged if used outside the loop `m->global`).
|
||
- **Inline-limit lever (byte-proven func_80150528, §30a#3):** an invariant loop limit
|
||
written INLINE (`D_x + K`) in both guard and condition → the in-loop limit insns
|
||
are movables hoisted to the preheader → preheader copy + temp→callee-saved regalloc.
|
||
A cached `u8 *end` local computes it ONCE before the loop instead → different
|
||
(fewer) insns, wrong regalloc.
|
||
|
||
**Tells:** `la`/`lui+addiu` pair right before `NOTE_INSN_LOOP_BEG`'s position
|
||
(preheader) = moved movable (dump `moved to N`) · in-loop `la` at a call-arg =
|
||
single-use substitution (NOT a bug) · in-loop reload of a plainly-invariant global +
|
||
a `jal` anywhere in the loop = unknown_address_altered (INTRINSIC — write the
|
||
uncached access) · dump `not desirable` = threshold fail (rare; big loop, low savings).
|
||
|
||
---
|
||
|
||
## L5 — GIV REPLACEABILITY / FINAL-VALUE COMPENSATION — **DIAGNOSTIC (route permuter if fighting placement)**
|
||
|
||
**Source:** `record_giv` replaceability (2.7.2:4426-4500), `check_final_value`
|
||
(:4551), copy emission + final values in `strength_reduce` (:3940-3990 —
|
||
non-replaceable giv gets `mov old,new` INSIDE the loop after its insn (:3948);
|
||
`final_value` giv/biv gets `reg = final` after loop end, or BEFORE the loop when it
|
||
has multiple exits).
|
||
|
||
**Mechanism/tells:**
|
||
- DEST_REG giv whose lifetime crosses a biv increment, is used outside the loop, or
|
||
sits behind spaghetti branches → NOT replaceable → reduced IV **plus a `move`
|
||
inside the loop**. Tell: `move rOld, rIV` right after the giv's defining position.
|
||
- Eliminated/reduced regs live after the loop → **post-loop set insns**
|
||
(`reg = final_value`), and anchor pre-bias produces **pre-bump/post-adjust pairs**:
|
||
proven expF f2 — `addu $3,$3,16` before entry, `lw -16($3)` inside,
|
||
`addu $3,$3,-16` after exit. A "useless" `addiu rX,rX,±K` straddling the loop
|
||
boundary is loop.c final-value machinery, not scheduling noise.
|
||
- C lever (soft): keep a derived pointer's uses on ONE side of the `p += K` to avoid
|
||
the crossing (or place the increment where the target implies — see L8). If the
|
||
target shows the compensation insns, your C must CREATE the crossing.
|
||
|
||
## L6 — REDUCED-GIV INCREMENT PLACEMENT — **INTRINSIC (order), sched-owned**
|
||
|
||
`strength_reduce` (2.7.2:~3900, `emit_iv_add_mult(..., tv->insn)`) always inserts each reduced giv's `addiu` immediately
|
||
BEFORE the biv's increment insn → giv-adds cluster at the biv update in RTL order;
|
||
sched1 then fills delay slots (expF f1: q's add landed in the branch delay slot).
|
||
Which adds exist = L1 (steerable); their final ORDER = scheduler domain. Don't grind
|
||
add-order here; fix the IV set first, then route residual order to permuter/sched
|
||
agent.
|
||
|
||
## L7 — WHOLE LOOP SKIPPED (find_and_verify_loops) — **STEERABLE (structure)**
|
||
|
||
**Source:** `find_and_verify_loops` (2.7.2:2222): a loop is `loop_invalid` if jumped
|
||
into from outside (label reachable from elsewhere), contains `setjmp`, or its label is
|
||
used in an initializer/computed goto. Invalid → NO hoisting, NO strength reduction,
|
||
NO reversal for that loop (inner loops still done).
|
||
**Tell:** a loop where obviously-invariant address computations sit unreduced inside
|
||
(pattern matches -O0-ish body inside an -O2 fn) + irregular entry (goto into body,
|
||
switch dispatching into a loop). **Lever:** reproduce the target's entry structure;
|
||
if the target LOOKS unoptimized inside a loop, check for an into-loop jump in the asm
|
||
before assuming a compiler quirk.
|
||
|
||
## L8 — `not_every_iteration` / INCREMENT POSITION — **STEERABLE**
|
||
|
||
**Source:** scan loops in `strength_reduce` (2.7.2: both copies set
|
||
`not_every_iteration` after **any label OR jump** — 2.7.2 is more conservative than
|
||
2.8.1 which exempts exit jumps); reset when a label with no more labels to loop-end is
|
||
passed (:3403/:3680); `record_biv`/`record_giv` `always_executed`/`always_computable`;
|
||
`update_giv_derive` (:4693).
|
||
|
||
**Mechanism:** everything AFTER the first branch inside the body is "not every
|
||
iteration": DEST_REG givs there lose `always_computable` → can't back biv elimination
|
||
(L2) or derive other givs; biv increments there make the biv not-always-executed →
|
||
blocks reversal-adjacent reasoning and auto-inc-style tightness.
|
||
**C lever:** the POSITION of `p += K` / `i++` in the body (top vs bottom, before vs
|
||
after the `if`s) and `continue` placement (NOTE_INSN_LOOP_CONT) are free C-side knobs
|
||
that flip these flags. If elimination/reduction behaves differently than a sibling
|
||
function, compare where the increment sits relative to branches.
|
||
|
||
---
|
||
|
||
## What is NOT a loop.c residual (routing)
|
||
|
||
- **Store-vs-load stuck order** → `/s` `MEM_IN_STRUCT_P` aliasing (expr.c agent,
|
||
cookbook §30/§30a#1). loop.c only consumes it via `true_dependence` in
|
||
`invariant_p` (L4) and `note_addr_stored` dedup.
|
||
- **Cross-jump tail-merge / per-branch coalescing** (e.g. `func_80148094` — its
|
||
@stuck diagnosis is `-fac` reg coalescing + tail merge, NOT its loops) → jump.c/
|
||
regalloc agents; loop levers won't move it.
|
||
- **`while` vs `do-while` guard duplication** (`NOTE_INSN_LOOP_VTOP`) → jump.c
|
||
(`duplicate_loop_exit_test`); loop.c only reads VTOP to reset `maybe_never`.
|
||
- **-funroll-loops** paths (`unroll.c`) are dead at -O2; unroll.c only contributes
|
||
`loop_iterations`/`final_[bg]iv_value` helpers to the above.
|
||
|
||
## Verdict summary
|
||
|
||
| class | verdict | proof |
|
||
|---|---|---|
|
||
| L1 IV count/anchor | **STEERABLE** | ×134 func_80150528/func_8014F2E0 (§30a) + live counter-proofs twoptr/anchor20 (this file) |
|
||
| L2 index-biv elimination | **STEERABLE** | cc1 expA/expG/expH (deterministic asm); mechanism source-pinned |
|
||
| L3 reversal (bgez down-count) | **STEERABLE** | cc1 expB/expC-c2 |
|
||
| L4 hoist/preheader/la-in-loop | **STEERABLE** (call-loop mem reloads INTRINSIC — match by not caching) | ×134 func_80150528 (inline-limit §30a#3) + cc1 expC |
|
||
| L5 final-value compensation | DIAGNOSTIC / soft lever | cc1 expF-f2 |
|
||
| L6 giv-add order | INTRINSIC → permuter/sched | expF-f1 |
|
||
| L7 invalid loops | STEERABLE (structure) | source-pinned |
|
||
| L8 increment position | STEERABLE | source-pinned (flags feed L2/L3) |
|
||
|
||
Experiment corpus: `.run/gccmap/exp/{expA..expH}.c` + `cc.sh` (rerunnable one-liners);
|
||
counter-proof C for the banked exemplar: `.run/gccmap/exp/func_80150528_{twoptr,anchor20}.c`.
|
||
|
||
### Movables EMISSION ORDER — the bitfield mask decoupler (Phase 24 T7, cookbook §36, byte-proven func_8013AF20) — **STEERABLE**
|
||
`move_movables` emits hoisted const-sets in the order `scan_loop` FOUND them = loop-body insn
|
||
order. A user-mask statement materializes its constants in C expression order — COUPLED to the
|
||
AND/OR compute shape (reordering `&`-operands flips both). The one C form that materializes
|
||
`0x00ffffff` BEFORE `0xff000000` while still computing `dest & 0xff000000` first is the
|
||
**BITFIELD store** (libgpu `setaddr`/`addPrim`): `store_fixed_bit_field` masks the VALUE first
|
||
(expmed.c:667 `must_and` → :679-681), the dest second (:694-696), `ior` dest-first (:706).
|
||
Downstream, preheader birth order sets the K2 tie-break (later-born = shorter LL = higher
|
||
priority), and a lui+ori const additionally SKIPS local-alloc.c:1064's LL-doubling (sched.c:4830
|
||
`try_split` → mips.md:3208 `large_int` split → `reg_n_sets==2` fails the :1021 gate) — see
|
||
regalloc.md RC-7. Full chain + gdb numbers: cookbook §36.
|
||
|
||
### The giv-init fence — force `emit_iv_add_mult`'s giv-init MOVE (Phase 24 T5, cookbook §34) — **STEERABLE**
|
||
When a loop's counter-derived pointer (a general induction var) has its init COALESCED with the invariant address, gcc drops one instruction → a full count mismatch vs a target that kept the `addu dst,base,$zero`. **Fix:** `asm("":"=r"(base):"0"(base)); dst = base;` forces `emit_iv_add_mult`'s giv-init move (`loop.c:5556 if (reg != result) emit_move_insn(reg,result)`) to materialize = the target's `addu dst,base,$zero`. The general fix for the "gcc coalesced the giv init, dropping an instruction" class on any giant with a counter-derived pointer.
|