# 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** | > **[A23] AUDIT OUTCOME (2026-07-28):** all 96 claims in this file were re-derived against > `tools/reference/gcc-2.7.2/` by parallel agents, each REFUTED claim adversarially re-checked. > **Only 3 REFUTED were raised — the LOWEST error density of any map file**, which is a direct credit > to the caveat table above: it already captured the behavioural 2.8.1-vs-2.7.2 deltas. Two upheld > corrections are marked `[A23]` inline (the "no memory load is EVER hoisted" absolute, and the > right-to-left call-arg order). **One honest gap:** 12 findings in the biv-elimination / > `check_dbra_loop` area returned evidence that quoted THIS FILE rather than the compiler source, so > they are **unverified, not confirmed** — the caveat table's own rows are the ones affected. Re-derive > them before leaning on a biv-elimination claim. > **[A23-CORRECTION, same day]** The sentence above is WITHDRAWN. Those 12 findings are **VERIFIED, > not unverified** — the failure was in MY checker, not the agents' evidence: it used Python > `splitlines()`, which splits on the FORM FEED (`\f`) page separators GNU C sources are full of > (loop.c has 47), so every computed line number after the first `\f` was shifted by up to 47 — > beyond the checker's own ±40 window, which turned real quotes into "FABRICATED". Re-run after the > fix: **299/299 exact, 0 fabricated.** The agents' line numbers were right all along. 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 [2.7.2]`: `(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<giv` is prepend-built during the forward scan, and `combine_givs` (2.7.2 loop.c:5494 [2.7.2]) 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*~~ — **[A23] FALSE: args are emitted LEFT-TO-RIGHT.** The audit's RTL dump shows `$a0` at insn 10, `$a1` at 12, `$a2` at 14 (ascending), so the FIRST arg's load is emitted **first**. The observed fact that banked `func_80150528` anchors at +0x20 (its first call arg) still holds — but it follows from `record_giv`'s prepend + `combine_givs` taking the list HEAD (which makes the last-PREPENDED giv the head), NOT from a right-to-left arg order. Do not reason about arg emission order from this bullet. **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<= 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~~ **most** loads. ~~**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.~~ **[A23] "EVER" is FALSE — there is a real exception.** `invariant_p`'s `case MEM:` arm (2.7.2 `loop.c:2760-2775 [2.7.2]`) checks `RTX_UNCHANGING_P (x)` **before** consulting `unknown_address_altered` and `break`s — i.e. **read-only items ARE invariant and DO hoist even with a call in the loop.** Byte-proven during the audit on the pinned cc1: `int t(const int *p,int n){int s=0,i;for(i=0;iglobal`). - **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 [2.7.2] `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 [2.7.2]'s LL-doubling (sched.c:4830 [2.7.2] `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 [2.7.2] 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.