/* Source code for RCC-Legacy. Copyright (C) 2026 Richard Qin This file is part of RCC. RCC is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. RCC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with RCC; see the file LICENSE. If not see . */ /// @file unswitch.cc /// @brief Loop Unswitching. /// /// See docs/unswitch.md for architectural notes. #include "rcc/all.h" #include #include #include #include #include namespace rcc { namespace { constexpr int kUnswitchMaxBodyIns = 64; constexpr int kUnswitchFixpointIters = 3; // Loop Identification struct LoopShape { Blk* header; Blk* preheader; Blk* latch; Blk* exit_bb; std::vector body; // includes header / latch }; // Find simple loops (derived from unroll.cc). std::vector find_simple_loops(Fn* fn) { std::vector out; for (Blk* b = fn->start; b; b = b->link) b->visit = ~0u; for (uint i = 0; i < fn->nblk; i++) { Blk* hd = fn->rpo[i]; if (hd->id == ~0u) continue; int back_cnt = 0, pre_cnt = 0; uint latch_idx = 0, pre_idx = 0; for (uint k = 0; k < hd->npred; k++) { Blk* p = hd->pred[k]; if (p->id == ~0u) continue; if (p->id >= i) { back_cnt++; latch_idx = k; } else { pre_cnt++; pre_idx = k; } } if (back_cnt != 1 || pre_cnt != 1) continue; Blk* latch = hd->pred[latch_idx]; Blk* preheader = hd->pred[pre_idx]; // Reverse DFS to collect body std::vector body = {hd}; std::vector stack = {latch}; hd->visit = hd->id; while (!stack.empty()) { Blk* n = stack.back(); stack.pop_back(); if (n->visit == hd->id) continue; n->visit = hd->id; body.push_back(n); for (uint k = 0; k < n->npred; k++) stack.push_back(n->pred[k]); } // single-exit gate if (hd->jmp.type != Jjnz) continue; Blk* exit_bb = hd->s2; if (!exit_bb || !hd->s1) continue; auto in_body = [&](Blk* x) -> bool { for (Blk* bb : body) if (bb == x) return true; return false; }; if (in_body(exit_bb)) continue; if (!in_body(hd->s1)) continue; bool ok = true; for (Blk* bb : body) { if (bb == hd) continue; if (isret(bb->jmp.type)) { ok = false; break; } auto check = [&](Blk* s) -> bool { return !s || in_body(s); }; if (!check(bb->s1) || !check(bb->s2)) { ok = false; break; } } if (!ok) continue; LoopShape L; L.header = hd; L.preheader = preheader; L.latch = latch; L.exit_bb = exit_bb; L.body = std::move(body); out.push_back(std::move(L)); } return out; } // Loop Invariance bool ref_is_invariant(Fn* fn, const LoopShape& L, Ref r) { if (rtype(r) == RCon) return true; if (rtype(r) != RTmp) return false; if (r.val >= static_cast(fn->ntmp)) return false; uint bid = fn->tmp[r.val].bid; if (bid == ~0u) return true; for (Blk* b : L.body) if (b->id == bid) return false; return true; } // Find a hoistable Jjnz branch. Returns nullptr if no candidate. Blk* find_unswitch_blk(Fn* fn, const LoopShape& L) { for (Blk* b : L.body) { if (b == L.header) continue; // header.Jjnz is loop exit, cannot hoist if (b == L.latch) continue; // Jjnz on latch is usually back-edge directed, skip to simplify analysis if (b->jmp.type != Jjnz) continue; if (rtype(b->jmp.arg) != RTmp) continue; // RCon left to simplcfg if (!ref_is_invariant(fn, L, b->jmp.arg)) continue; if (!b->s1 || !b->s2 || b->s1 == b->s2) continue; bool s1_in = false, s2_in = false; for (Blk* bb : L.body) { if (bb == b->s1) s1_in = true; if (bb == b->s2) s2_in = true; } if (!s1_in || !s2_in) continue; return b; } return nullptr; } int count_body_ins(const LoopShape& L) { int n = 0; for (Blk* b : L.body) { for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) if (i->op != Onop) n++; for (Phi* p = b->phi; p; p = p->link) (void)p, n++; } return n; } // Clone Infrastructure (derived from unroll.cc with minor adaptations) struct CopyState { std::unordered_map blk_map; // body blk.id -> clone std::unordered_map tmp_map; // body-defined tmp.val -> clone Ref }; Ref remap_tmp(Ref r, const std::unordered_map& m) { if (rtype(r) != RTmp) return r; auto it = m.find(r.val); return (it != m.end()) ? it->second : r; } // Generic block clone. void clone_blk(Ctx* ctx, const CopyState& st, Blk* src, Blk* dst) { dst->phi = nullptr; Phi** tail = &dst->phi; for (Phi* sp = src->phi; sp; sp = sp->link) { Phi* np = static_cast(alloc(ctx, sizeof(Phi))); np->to = remap_tmp(sp->to, st.tmp_map); np->cls = sp->cls; np->visit = 0; np->narg = sp->narg; np->arg = static_cast(vnew(ctx, sp->narg, sizeof(Ref), PFn)); np->blk = static_cast(vnew(ctx, sp->narg, sizeof(Blk*), PFn)); for (uint k = 0; k < sp->narg; k++) { np->arg[k] = remap_tmp(sp->arg[k], st.tmp_map); auto it = st.blk_map.find(sp->blk[k]->id); np->blk[k] = (it != st.blk_map.end()) ? it->second : sp->blk[k]; } np->link = nullptr; *tail = np; tail = &np->link; } dst->ins = static_cast(vnew(ctx, 0, sizeof(Ins), PFn)); dst->nins = 0; for (Ins* si = src->ins; si < &src->ins[src->nins]; si++) { Ins ni; ni.op = si->op; ni.cls = si->cls; ni.to = remap_tmp(si->to, st.tmp_map); ni.arg[0] = remap_tmp(si->arg[0], st.tmp_map); ni.arg[1] = remap_tmp(si->arg[1], st.tmp_map); addins(ctx, &dst->ins, &dst->nins, &ni); } auto map_succ = [&](Blk* s) -> Blk* { if (!s) return nullptr; auto it = st.blk_map.find(s->id); return (it != st.blk_map.end()) ? it->second : s; }; dst->jmp.type = src->jmp.type; dst->jmp.arg = remap_tmp(src->jmp.arg, st.tmp_map); dst->s1 = map_succ(src->s1); dst->s2 = map_succ(src->s2); } CopyState clone_loop(Ctx* ctx, Fn* fn, const LoopShape& L, const char* tag) { CopyState st; std::unordered_set body_defined; for (Blk* b : L.body) { for (Phi* p = b->phi; p; p = p->link) if (rtype(p->to) == RTmp) body_defined.insert(p->to.val); for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) if (rtype(i->to) == RTmp && i->to.val != 0) body_defined.insert(i->to.val); } for (Blk* b : L.body) { Blk* nb = newblk(ctx); nb->name = strf(ctx, PFn, "%s.%s", tag, b->name ? b->name : "blk"); nb->id = fn->nblk++; st.blk_map[b->id] = nb; } for (uint tv : body_defined) { const char* prfx = fn->tmp[tv].name ? fn->tmp[tv].name : "u"; st.tmp_map[tv] = newtmp(ctx, prfx, fn->tmp[tv].cls, fn); } for (Blk* b : L.body) clone_blk(ctx, st, b, st.blk_map[b->id]); return st; } // Remove dangling phi incoming edges from deactivated successors. void remove_phi_entry(Blk* succ, Blk* old_pred) { for (Phi* p = succ->phi; p; p = p->link) { uint w = 0; for (uint k = 0; k < p->narg; k++) { if (p->blk[k] == old_pred) continue; if (w != k) { p->arg[w] = p->arg[k]; p->blk[w] = p->blk[k]; } w++; } p->narg = w; } } void do_unswitch(Ctx* ctx, Fn* fn, const LoopShape& L, Blk* unsw_blk) { Ref cond = unsw_blk->jmp.arg; Blk* orig_s1 = unsw_blk->s1; Blk* orig_s2 = unsw_blk->s2; CopyState A = clone_loop(ctx, fn, L, "uA"); CopyState B = clone_loop(ctx, fn, L, "uB"); Blk* header_A = A.blk_map.at(L.header->id); Blk* header_B = B.blk_map.at(L.header->id); Blk* unsw_A = A.blk_map.at(unsw_blk->id); Blk* unsw_B = B.blk_map.at(unsw_blk->id); // In A, collapse to true branch (orig.s1) unsw_A->jmp.type = Jjmp; unsw_A->jmp.arg = Ref::none(); unsw_A->s1 = A.blk_map.at(orig_s1->id); unsw_A->s2 = nullptr; // orig.s2 clone in A is no longer reached by unsw_A; must remove phi edges from unsw_A remove_phi_entry(A.blk_map.at(orig_s2->id), unsw_A); // In B, collapse to false branch (orig.s2) unsw_B->jmp.type = Jjmp; unsw_B->jmp.arg = Ref::none(); unsw_B->s1 = B.blk_map.at(orig_s2->id); unsw_B->s2 = nullptr; remove_phi_entry(B.blk_map.at(orig_s1->id), unsw_B); // guard block: Jjnz cond, header_A, header_B Blk* guard = newblk(ctx); guard->name = strf(ctx, PFn, "uG.%s", L.header->name ? L.header->name : ""); guard->id = fn->nblk++; guard->phi = nullptr; guard->ins = static_cast(vnew(ctx, 0, sizeof(Ins), PFn)); guard->nins = 0; guard->jmp.type = Jjnz; guard->jmp.arg = cond; guard->s1 = header_A; guard->s2 = header_B; // preheader -> guard (replaces original preheader -> header) if (L.preheader->s1 == L.header) L.preheader->s1 = guard; if (L.preheader->s2 == L.header) L.preheader->s2 = guard; // Update header_A / header_B phi.blk[k]==preheader to guard auto fix_hdr_pred = [&](Blk* hdr_clone) { for (Phi* p = hdr_clone->phi; p; p = p->link) for (uint k = 0; k < p->narg; k++) if (p->blk[k] == L.preheader) p->blk[k] = guard; }; fix_hdr_pred(header_A); fix_hdr_pred(header_B); // exit_bb phi: split blk==L.header incoming edges into two Blk* exit = L.exit_bb; for (Phi* p = exit->phi; p; p = p->link) { uint header_entries = 0; for (uint k = 0; k < p->narg; k++) if (p->blk[k] == L.header) header_entries++; if (header_entries == 0) continue; uint new_narg = p->narg + header_entries; // Expand each into 2 = +1 Ref* new_arg = static_cast(vnew(ctx, new_narg, sizeof(Ref), PFn)); Blk** new_blk = static_cast(vnew(ctx, new_narg, sizeof(Blk*), PFn)); uint w = 0; for (uint k = 0; k < p->narg; k++) { if (p->blk[k] == L.header) { new_arg[w] = remap_tmp(p->arg[k], A.tmp_map); new_blk[w] = header_A; w++; new_arg[w] = remap_tmp(p->arg[k], B.tmp_map); new_blk[w] = header_B; w++; } else { new_arg[w] = p->arg[k]; new_blk[w] = p->blk[k]; w++; } } p->narg = new_narg; p->arg = new_arg; p->blk = new_blk; } // link chain: preheader -> guard -> A.body -> B.body -> original preheader.link Blk* saved = L.preheader->link; L.preheader->link = guard; Blk** plink = &guard->link; for (Blk* b : L.body) { *plink = A.blk_map.at(b->id); plink = &(*plink)->link; } for (Blk* b : L.body) { *plink = B.blk_map.at(b->id); plink = &(*plink)->link; } *plink = saved; if (debug['W']) { FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; fprintf(out, "> unswitch %s: hdr b%u, unsw b%u, body=%zu\n", fn->name, L.header->id, unsw_blk->id, L.body.size()); } } bool unswitch_pass(Ctx* ctx, Fn* fn) { auto loops = find_simple_loops(fn); for (auto& L : loops) { Blk* u = find_unswitch_blk(fn, L); if (!u) continue; if (count_body_ins(L) > kUnswitchMaxBodyIns) continue; do_unswitch(ctx, fn, L, u); return true; // Process one per pass to avoid scanning mutated CFG } return false; } } // namespace void unswitch(Ctx* ctx, Fn* fn) { // Caller should have done fillcfg + filluse; filldom not needed (only Tmp.bid is used). int iter = 0; for (; iter < kUnswitchFixpointIters; iter++) { if (!unswitch_pass(ctx, fn)) break; fillcfg(ctx, fn); filluse(ctx, fn); } if (debug['W'] && iter > 0) { FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; fprintf(out, "> unswitch %s: %d round%s\n", fn->name, iter, iter == 1 ? "" : "s"); } } } // namespace rcc