25 KiB
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_looparea 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 Pythonsplitlines(), 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\fwas 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
-dato the cc1 line; on file inputfoo.iyou getfoo.i.loopetc. (.run/gccmap/exp/cc.shbuilds .s; copy its cc1 line +-dafor 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-= 3per moved insn (move_movables), andinsn_count *= 2for 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.2cse.c:680 [2.7.2]:(N)*4-2);copy_cost= 4. Benefit unit: 2 ≈ one insn.- Giv worth-while test (
strength_reduce): reduce ifflifetime × threshold × (benefit − 2×biv_count) ≥ insn_count. A barereg = biv<<kgiv 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)vsadd (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 [2.7.2]), 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):
- 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).
- Mem-address givs (
DEST_ADDR, from*(T*)(p+k)) combine iff identical (mult,add) orexpress_fromsucceeds — needs anchor g1's add_val to be CONST_INT, g2.mult % g1.mult == 0, result a legit address withADDRESS_COST(new) ≤ ADDRESS_COST(orig)(MIPSmips_address_cost: reg=1, reg+small16=1, reg+big=2, reg+(sym+off)=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 aslw $3,36860($4)macro). IV splits come from bivs or orphaned givs, not range. - 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. - 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 expDd3, expE e1/e2: cast/conditional/cross-bb loads all still dissolve). - Anchor choice:
bl->givis prepend-built during the forward scan, andcombine_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 becomeanchor+deltaoffsets.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$a0at insn 10,$a1at 12,$a2at 14 (ascending), so the FIRST arg's load is emitted first. The observed fact that bankedfunc_80150528anchors at +0x20 (its first call arg) still holds — but it follows fromrecord_giv's prepend +combine_givstaking 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-verifiedMATCH (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.cproduces 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 —
*qkeeps biv q alive in addition to the combined giv IV → 3 IVs (proven.run/gccmap/exp/func_80150528_anchor20.c: 57 ins, extraaddiu s1,s0,88). Write the zero-offset field as*(s32*)qonly 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
/slever (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— expAtest_const, 25×8); - variable/invariant-reg ARG →
emit_iv_add_mult(ARG,mult,add)atwhere = threshold ≥ insn_count ? preheader : test-site(2.7.2:5968) → small loops (≤31 insns w/ call, ≤62 without): preheadersll $4,n,3(expAtest_var); bigger loops: computed at the bottom, inside the loop.
When elimination FAILS (i kept → index + pointer, 2 IVs):
imaterialized anywhere else: call arg, stored, returned, compared non-linearly (maybe_eliminate_biv_1REG case returns 0) — expCc1;all_reduced == 0because ANY giv of the class was benefit-ignored — the classic shape: when the array base lives in a hoisted register (la rB,Amovable — happens when the loaded value feeds a call, or in big loops), the address givs haveadd (reg:N)(not const), they combine amongst THEMSELVES by identity, and the orphanedi<<kgiv (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_deadanchor.
C levers:
- Target = single scaled-index IV,
slt rIV, #Kendtest,sym+off(rIV)HI/LO-reloc loads → plainfor (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
sltuvs hoisted end → pointer formfor (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 → makeiload-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 (
sllbefore the loop vs at its bottom) follows thewhereflip — 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
LTvs CONST_INT; increment const;bound % inc == 0; initial_value == const0_rtxliterally (start at 0 — no normalization in 2.7.2);- counting-only biv (
giv_count==0and 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_psrc can't move past a conditional/call. Moved iffthreshold × 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) == 0foreverymost loads.no memory load is EVER hoisted from a loop containing a call (proven expC c3:[A23] "EVER" is FALSE — there is a real exception.lw D_SRCstays in-loop). Don't fight it with cached locals — match the target's in-loop reloads by NOT caching.invariant_p'scase MEM:arm (2.7.2loop.c:2760-2775 [2.7.2]) checksRTX_UNCHANGING_P (x)before consultingunknown_address_alteredandbreaks — 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;}putslw $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 forconst/RTX_UNCHANGING_Pones — 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_dependencewith anyloop_store_memsentry (invariant_p:2695;/s/MEM_IN_STRUCT_Penters here — expr.c agent's domain, cookbook §30/§30a#1) — proven expC c2 (lw D_SRChoisted over thedst[i]stores). - Single-use invariant in a CALL loop is SUBSTITUTED into its use and the set
deleted (
scan_loop:735, only whenloop_has_call) → the classic "la $a0,SYMINSIDE 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-looplabefore ajalis 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 loopm->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 cachedu8 *endlocal 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
moveinside the loop. Tell:move rOld, rIVright 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,16before entry,lw -16($3)inside,addu $3,$3,-16after exit. A "useless"addiu rX,rX,±Kstraddling 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 += Kto 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 ifs) 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 →
/sMEM_IN_STRUCT_Paliasing (expr.c agent, cookbook §30/§30a#1). loop.c only consumes it viatrue_dependenceininvariant_p(L4) andnote_addr_storeddedup. - Cross-jump tail-merge / per-branch coalescing (e.g.
func_80148094— its @stuck diagnosis is-facreg coalescing + tail merge, NOT its loops) → jump.c/ regalloc agents; loop levers won't move it. whilevsdo-whileguard duplication (NOTE_INSN_LOOP_VTOP) → jump.c (duplicate_loop_exit_test); loop.c only reads VTOP to resetmaybe_never.- -funroll-loops paths (
unroll.c) are dead at -O2; unroll.c only contributesloop_iterations/final_[bg]iv_valuehelpers 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.