mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-27 22:45:39 -04:00
86caa7dfdb
T36 CORRECTION (the important half). Building the cookbook sweep tool surfaced a defect in
tools/verify_map_findings.py, which I had already used to validate BOTH map audits:
- GNU C sources use FORM FEED (\f) page separators — loop.c 47, cse.c 36, reload1.c 27,
local-alloc.c 21. Python's splitlines() splits on \f; grep/sed do not. Every line number computed
after the first \f was shifted (up to 47 in loop.c), which is LARGER than the checker's own +/-40
window — precisely how a real quote gets reported FABRICATED.
- Re-run after the fix: T34 regalloc 27 OK/153 NEAR/0 FAB -> 180 OK/0/0. T35 four-file
47 OK/240 NEAR/12 FAB -> 299 OK/0/0. THE AGENTS' LINE NUMBERS WERE EXACT ALL ALONG. I had even
written the false "off by +2..+19" claim into the T35 agent prompt.
- MY DIAGNOSIS OF THE 12 WAS ALSO WRONG. I said agents pasted map prose into source_quote and
"verified" it by grepping — but I grepped claim_excerpt (which IS map prose) instead of
source_quote. The real source_quote was ` record_jump_equiv (insn, 0);` at cse.c:7511, a
correctly-located C line. Two stacked errors: a broken tool, then a check of the wrong field that
appeared to confirm it.
- Fixed: both tools use split("\n"); verify_map_findings.py documents the trap so it cannot return;
loop.md's "12 unverified" note is WITHDRAWN in place. Nothing was deleted on this basis (all 12
were CONFIRMED-status, none underpinned a refutation), and the T34/T35 upheld/overturned splits are
unaffected — those came from adversarial agents, not the checker.
T37 THE COOKBOOK SWEEP (what was asked for). New tools/sweep_citations.py puts the mechanical half of
a citation audit into zero-token tooling (offline-tooling-first): symbol-form cites are compared to
the real 2.7.2 definition line; file-form cites are localised to their enclosing function.
- matching-cookbook.md: 57 resolvable citations, all localisable. MIXED provenance but mostly sound —
materially better than the map files. loop.c:5556, local-alloc.c:1765/1795/1825, global.c:906/917/
924/1000, local-alloc.c:1021/1064, global.c:588/594, sched.c:820, expmed.c:556, jump.c:2131 all
land where the prose says. GENUINE MISS: expr.c:5535 is MIN/MAX optab code; the /s grant sites are
4577 and 4904.
- STATED LIMITATION: "lands in the right function" is weak for giants (expand_expr 4026->~6300,
jump_optimize 139->~2200). This is a CITATION sweep, not a claim audit — proportionate because the
cookbook's idioms are byte-proven and its cites are explanation. No idiom re-litigated.
Docs+tools only: no src/ or config/ touched; R22 not re-run and not claimed.
370 lines
24 KiB
Markdown
370 lines
24 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** |
|
||
|
||
> **[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`:
|
||
`(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*~~ — **[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<<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~~ **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`) 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;i<n;i++){s+=*p;f(i);}return s;}` puts
|
||
`lw $19,0($4)` in the **PREHEADER**, not the loop body.
|
||
**So the advice "don't fight it with cached locals" is right for ordinary loads and wrong for
|
||
`const`/`RTX_UNCHANGING_P` ones** — if the TARGET hoists a load out of a call-containing loop,
|
||
a const-qualified pointer is the lever, not evidence that you mis-read the loop.
|
||
- 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.
|