/* 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 unroll.cc /// @brief Loop Unrolling. /// /// See docs/unroll.md for architectural notes. #include "rcc/all.h" #include #include #include #include #include #include namespace rcc { namespace { constexpr int kFullUnrollTripMax = 16; constexpr int kFullUnrollBudget = 256; constexpr int kUnrollFixpointIters = 4; constexpr int kPartialFactor = 4; constexpr int kPartialMaxBodyIns = 64; // F * body_ins <= 256 total instruction budget // Loop Identification struct LoopShape { Blk* header; Blk* preheader; Blk* latch; Blk* exit_bb; uint preheader_idx; // index in header->pred where preheader sits uint latch_idx; // index in header->pred where latch sits std::vector body; // includes header, latch }; // Helper: find_simple_loops (see docs/unroll.md) 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]; // single back-edge int back_cnt = 0; uint latch_pred_idx = 0; uint pre_pred_idx = 0; int pre_cnt = 0; for (uint k = 0; k < hd->npred; k++) { if (hd->pred[k]->id >= i && hd->pred[k]->id != ~0u) { back_cnt++; latch_pred_idx = k; } else { pre_cnt++; pre_pred_idx = k; } } if (back_cnt != 1 || pre_cnt != 1) continue; // Require single latch + single preheader Blk* latch = hd->pred[latch_pred_idx]; Blk* preheader = hd->pred[pre_pred_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 (see docs/unroll.md) if (hd->jmp.type != Jjnz) continue; Blk* exit_bb = hd->s2; if (!exit_bb) 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; // header already checked auto check_succ = [&](Blk* s) -> bool { if (!s) return true; return in_body(s); }; if (!check_succ(bb->s1) || !check_succ(bb->s2)) { ok = false; break; } // Forbid Ocall / nested ret jmp in body if (isret(bb->jmp.type)) { ok = false; break; } for (Ins* ii = bb->ins; ii < &bb->ins[bb->nins]; ii++) { if (ii->op == Ocall) { ok = false; break; } } if (!ok) break; } if (!ok) continue; // header itself cannot have Ocall either (although Jjnz is not ret) for (Ins* ii = hd->ins; ii < &hd->ins[hd->nins]; ii++) { if (ii->op == Ocall) { ok = false; break; } } if (!ok) continue; LoopShape L; L.header = hd; L.preheader = preheader; L.latch = latch; L.exit_bb = exit_bb; L.preheader_idx = pre_pred_idx; L.latch_idx = latch_pred_idx; L.body = std::move(body); out.push_back(std::move(L)); } return out; } // Induction Variable Detection + Trip Count struct Induction { int64_t init; int64_t step; // > 0 int64_t bound; // Valid only when bound_const is true Ref bound_ref; // cmp's arg[1]: RCon or loop-invariant RTmp bool bound_const; // bound_ref is compile-time constant int cmp_op; // Ocsltw/Ocsltl/Ocslew/Ocslel int cmp_cls; // cls of cmp ins (for copying guard in partial unroll) Ref iv_ref; // induction variable header phi.to (used to find corresponding new phi in // main.head) int64_t trip; // Calculated when bound_const is true; -1 for runtime bounds }; bool ref_is_const(Fn* fn, Ref r, int64_t& out_val) { if (rtype(r) != RCon) return false; if (r.val >= fn->ncon) return false; Con* c = &fn->con[r.val]; if (c->type != Con::CBits) return false; if (c->flt) return false; out_val = c->bits.i; return true; } bool is_one_of(int op, std::initializer_list opts) { for (int o : opts) if (op == o) return true; return false; } // Find def of cmp. Returns Ins* or nullptr. Ins* find_def_ins(Fn* fn, Ref r) { if (rtype(r) != RTmp) return nullptr; if (r.val >= fn->ntmp) return nullptr; return fn->tmp[r.val].def; } // Check if ref is defined outside the loop (including constants). Uses Tmp.bid (set by filluse). bool def_outside_body(Fn* fn, const LoopShape& L, Ref r) { if (rtype(r) != RTmp) return true; // Constants / non-tmps are loop-invariant uint bid = fn->tmp[r.val].bid; for (Blk* bb : L.body) if (bb->id == bid) return false; return true; } // Resolve self-loop phis to their loop-invariant preheader value. Ref resolve_loop_invariant(Fn* fn, const LoopShape& L, Ref r) { for (int guard = 0; guard < 8; guard++) { if (def_outside_body(fn, L, r)) return r; // Defined inside loop: can only resolve further if it's a self-loop phi in header if (rtype(r) != RTmp || fn->tmp[r.val].bid != L.header->id) return Ref::none(); Phi* p = nullptr; for (Phi* q = L.header->phi; q; q = q->link) if (req(q->to, r)) { p = q; break; } if (!p) return Ref::none(); // Inside header but not a phi (it's a regular ins) -> true // variable Ref pre_arg = Ref::none(); bool selfish = true; for (uint k = 0; k < p->narg; k++) { if (p->blk[k] == L.preheader) pre_arg = p->arg[k]; else if (!req(p->arg[k], p->to)) selfish = false; // Non-self-loop incoming edge -> actually varying } if (!selfish || req(pre_arg, Ref::none())) return Ref::none(); r = pre_arg; // Resolved one level, continue (preheader value might also be a self-loop // phi) } return Ref::none(); } bool detect_induction(Fn* fn, const LoopShape& L, Induction& out) { // Header must end with Jjnz, arg = cmp tmp if (rtype(L.header->jmp.arg) != RTmp) return false; Ins* cmp = find_def_ins(fn, L.header->jmp.arg); if (!cmp) return false; if (!is_one_of(cmp->op, {Ocsltw, Ocsltl, Ocslew, Ocslel})) return false; // Parse bound (see docs/unroll.md) if (rtype(cmp->arg[0]) != RTmp) return false; Ref bound_ref = resolve_loop_invariant(fn, L, cmp->arg[1]); if (req(bound_ref, Ref::none())) return false; int64_t bound_v = 0; bool bound_const = ref_is_const(fn, bound_ref, bound_v); // Find matching header phi Phi* ind_phi = nullptr; for (Phi* p = L.header->phi; p; p = p->link) { if (req(p->to, cmp->arg[0])) { ind_phi = p; break; } } if (!ind_phi) return false; // Get init (preheader side) and i_next (latch side) if (ind_phi->narg != L.header->npred) return false; // Defensive check Ref init_ref = Ref::none(), i_next_ref = Ref::none(); for (uint k = 0; k < ind_phi->narg; k++) { if (ind_phi->blk[k] == L.preheader) init_ref = ind_phi->arg[k]; else if (ind_phi->blk[k] == L.latch) i_next_ref = ind_phi->arg[k]; } int64_t init_v; if (!ref_is_const(fn, init_ref, init_v)) return false; if (rtype(i_next_ref) != RTmp) return false; // i_next def must be Oadd(i, step_const) or Oadd(step_const, i) Ins* next_def = find_def_ins(fn, i_next_ref); if (!next_def) return false; if (next_def->op != Oadd) return false; int64_t step_v = 0; if (req(next_def->arg[0], ind_phi->to) && ref_is_const(fn, next_def->arg[1], step_v)) { // i_next = i + step } else if (req(next_def->arg[1], ind_phi->to) && ref_is_const(fn, next_def->arg[0], step_v)) { // i_next = step + i (commutative) } else { return false; } if (step_v <= 0) return false; // Only positive step supported for now out.init = init_v; out.step = step_v; out.bound = bound_v; out.bound_ref = bound_ref; out.bound_const = bound_const; out.cmp_op = cmp->op; out.cmp_cls = cmp->cls; out.iv_ref = ind_phi->to; if (!bound_const) { out.trip = -1; // Runtime bound: only usable for partial unroll return true; } // trip count depends on cmp_op int64_t span = bound_v - init_v; if (cmp->op == Ocslew || cmp->op == Ocslel) span += 1; if (span <= 0) { out.trip = 0; return true; } out.trip = (span + step_v - 1) / step_v; return true; } 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 / Unroll struct CopyState { std::unordered_map blk_map; // body blk.id -> clone std::unordered_map tmp_map; // body-defined tmp.val -> clone Ref Blk* first_blk; // = blk_map[L.header.id] Blk* last_blk; // = blk_map[L.latch.id] }; Ref remap_tmp(Ref r, const std::unordered_map& m) { if (rtype(r) != RTmp) return r; auto it = m.find(r.val); if (it == m.end()) return r; return it->second; } // Deep copy a body block into dst. Discard header phis, map header Jjnz to Jjmp. void clone_body_blk(Ctx* ctx, const CopyState& st, Blk* src, Blk* dst, bool is_header, bool is_latch) { // ── phi ── dst->phi = nullptr; if (!is_header) { 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; } } // ── ins ── 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); } // ── jmp ── if (is_header) { // Jjnz cmp, body_first, exit -> Jjmp blk_map[body_first.id] dst->jmp.type = Jjmp; dst->jmp.arg = Ref::none(); auto it = st.blk_map.find(src->s1->id); assert(it != st.blk_map.end()); dst->s1 = it->second; dst->s2 = nullptr; } else if (is_latch) { // Kept for now, caller rewrites when linking across copies dst->jmp.type = Jjmp; dst->jmp.arg = Ref::none(); dst->s1 = nullptr; dst->s2 = nullptr; } else { dst->jmp.type = src->jmp.type; dst->jmp.arg = remap_tmp(src->jmp.arg, st.tmp_map); dst->s1 = nullptr; dst->s2 = nullptr; if (src->s1) { auto it = st.blk_map.find(src->s1->id); dst->s1 = (it != st.blk_map.end()) ? it->second : src->s1; } if (src->s2) { auto it = st.blk_map.find(src->s2->id); dst->s2 = (it != st.blk_map.end()) ? it->second : src->s2; } } } // Clone body N times sequentially to replace original loop. Matching by Blk* pointer is the only // robust way. Ref phi_arg_for_pred(Phi* p, Blk* pred) { for (uint k = 0; k < p->narg; k++) if (p->blk[k] == pred) return p->arg[k]; die("phi_arg_for_pred: pred not found in phi.blk[]"); } // Prerequisites: detect_induction successful, N = induction.trip. void full_unroll(Ctx* ctx, Fn* fn, const LoopShape& L, int64_t N) { // Collect body-defined tmps 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); } // Collect header phi list (in linked list order, to re-define them later) std::vector header_phis; for (Phi* p = L.header->phi; p; p = p->link) header_phis.push_back(p); // Clone for each iteration std::vector copies(static_cast(N)); // Map previous latch tmps std::unordered_map prev_latch_tmp_map; static int unroll_seq = 0; int my_seq = unroll_seq++; for (int64_t k = 0; k < N; k++) { CopyState& st = copies[static_cast(k)]; // Allocate new blk for each body block for (Blk* b : L.body) { Blk* nb = newblk(ctx); char buf[128]; std::snprintf(buf, sizeof(buf), "u%d.%lld.%s", my_seq, static_cast(k), b->name ? b->name : "blk"); nb->name = strf(ctx, PFn, "%s", buf); nb->id = fn->nblk++; st.blk_map[b->id] = nb; } st.first_blk = st.blk_map[L.header->id]; st.last_blk = st.blk_map[L.latch->id]; // Assign fresh ids to body-defined tmps; then populate header phi.tos with entry values 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 (Phi* p : header_phis) { Ref entry_val; if (k == 0) { entry_val = phi_arg_for_pred(p, L.preheader); } else { Ref prev_latch_ref = phi_arg_for_pred(p, L.latch); entry_val = remap_tmp(prev_latch_ref, prev_latch_tmp_map); } if (rtype(p->to) == RTmp) st.tmp_map[p->to.val] = entry_val; } // Clone each body block for (Blk* b : L.body) { Blk* dst = st.blk_map[b->id]; bool is_h = (b == L.header); bool is_l = (b == L.latch); // Single-block loop specialization clone_body_blk(ctx, st, b, dst, is_h, is_l && !is_h); } // Save current tmp_map for next round prev_latch_tmp_map = st.tmp_map; } // Link copies: copy_k's latch_clone.jmp = Jjmp copy_{k+1}.first for (int64_t k = 0; k < N - 1; k++) { Blk* lc = copies[static_cast(k)].last_blk; Blk* nh = copies[static_cast(k + 1)].first_blk; // Single-block loop self-loop fix: rewrite last_blk's Jjmp to point to next copy lc->jmp.type = Jjmp; lc->jmp.arg = Ref::none(); lc->s1 = nh; lc->s2 = nullptr; } // Last round's latch_clone: append Ocopy to re-def header phi.tos, jmp points to exit_bb { CopyState& last = copies[static_cast(N - 1)]; Blk* lc = last.last_blk; for (Phi* p : header_phis) { Ref final_val = remap_tmp(phi_arg_for_pred(p, L.latch), last.tmp_map); Ins re_def; re_def.op = Ocopy; re_def.cls = static_cast(p->cls) & 3u; re_def.to = p->to; // Original header phi.to (now dead, SSA def monopolized) re_def.arg[0] = final_val; re_def.arg[1] = Ref::none(); addins(ctx, &lc->ins, &lc->nins, &re_def); } lc->jmp.type = Jjmp; lc->jmp.arg = Ref::none(); lc->s1 = L.exit_bb; lc->s2 = nullptr; } // Point preheader to copy_0.first; clear original header's pred references (leave for fillcfg // to rebuild) Blk* new_entry = copies[0].first_blk; if (L.preheader->s1 == L.header) L.preheader->s1 = new_entry; if (L.preheader->s2 == L.header) L.preheader->s2 = new_entry; // Fix exit_bb phi incoming edges: replace header with last copy's latch_clone Blk* new_last = copies[static_cast(N - 1)].last_blk; for (Phi* p = L.exit_bb->phi; p; p = p->link) { for (uint kk = 0; kk < p->narg; kk++) { if (p->blk[kk] == L.header) p->blk[kk] = new_last; } } // Link chain: preheader -> copy_0_blocks -> ... -> copy_{N-1}_blocks -> original preheader.link Blk* saved_link = L.preheader->link; Blk** plink = &L.preheader->link; for (int64_t k = 0; k < N; k++) { const CopyState& st = copies[static_cast(k)]; // Link in L.body order for (Blk* b : L.body) { Blk* nb = st.blk_map.at(b->id); *plink = nb; plink = &nb->link; } } *plink = saved_link; if (debug['U']) { FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; fprintf(out, "> unroll full N=%lld in %s (header b%u, body=%zu, ins=%d)\n", static_cast(N), fn->name, L.header->id, L.body.size(), count_body_ins(L)); } } // Partial Unroll Blk* partial_unroll(Ctx* ctx, Fn* fn, const LoopShape& L, const Induction& ind, int F) { // body-defined tmps 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); } // header phi list std::vector header_phis; for (Phi* p = L.header->phi; p; p = p->link) header_phis.push_back(p); static int unroll_seq_p = 0; int my_seq = unroll_seq_p++; // Create main_head, build its phi list first (loopback edge filled later) Blk* main_head = newblk(ctx); main_head->name = strf(ctx, PFn, "u%d.main.head", my_seq); main_head->id = fn->nblk++; std::unordered_map head_to_main; // original phi.to.val -> main_head phi.to std::vector main_phis; Phi** ptail = &main_head->phi; for (Phi* p : header_phis) { Phi* pm = static_cast(alloc(ctx, sizeof(Phi))); pm->cls = p->cls; pm->visit = 0; const char* prfx = (rtype(p->to) == RTmp && fn->tmp[p->to.val].name) ? fn->tmp[p->to.val].name : "u"; pm->to = newtmp(ctx, prfx, p->cls, fn); pm->narg = 2; pm->arg = static_cast(vnew(ctx, 2, sizeof(Ref), PFn)); pm->blk = static_cast(vnew(ctx, 2, sizeof(Blk*), PFn)); pm->arg[0] = phi_arg_for_pred(p, L.preheader); pm->blk[0] = L.preheader; pm->arg[1] = Ref::none(); pm->blk[1] = nullptr; pm->link = nullptr; *ptail = pm; ptail = &pm->link; main_phis.push_back(pm); if (rtype(p->to) == RTmp) head_to_main[p->to.val] = pm->to; } // Clone body F times, same as full_unroll (except clone0 entry uses main_head phi.to) std::vector copies(static_cast(F)); std::unordered_map prev_latch_tmp_map; for (int k = 0; k < F; k++) { CopyState& st = copies[static_cast(k)]; for (Blk* b : L.body) { Blk* nb = newblk(ctx); nb->name = strf(ctx, PFn, "u%d.%d.%s", my_seq, k, b->name ? b->name : "blk"); nb->id = fn->nblk++; st.blk_map[b->id] = nb; } st.first_blk = st.blk_map[L.header->id]; st.last_blk = st.blk_map[L.latch->id]; 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 (Phi* p : header_phis) { Ref entry_val; if (k == 0) { auto it = head_to_main.find(p->to.val); entry_val = (it != head_to_main.end()) ? it->second : phi_arg_for_pred(p, L.preheader); } else { entry_val = remap_tmp(phi_arg_for_pred(p, L.latch), prev_latch_tmp_map); } if (rtype(p->to) == RTmp) st.tmp_map[p->to.val] = entry_val; } for (Blk* b : L.body) { Blk* dst = st.blk_map[b->id]; bool is_h = (b == L.header); bool is_l = (b == L.latch); clone_body_blk(ctx, st, b, dst, is_h, is_l && !is_h); } prev_latch_tmp_map = st.tmp_map; } // Link: copy_k.last -> copy_{k+1}.first for (int k = 0; k < F - 1; k++) { Blk* lc = copies[static_cast(k)].last_blk; lc->jmp.type = Jjmp; lc->jmp.arg = Ref::none(); lc->s1 = copies[static_cast(k + 1)].first_blk; lc->s2 = nullptr; } // Last latch -> main_head (back-edge) Blk* last_latch = copies[static_cast(F - 1)].last_blk; last_latch->jmp.type = Jjmp; last_latch->jmp.arg = Ref::none(); last_latch->s1 = main_head; last_latch->s2 = nullptr; // Fill main_head phi's loopback incoming edge for (size_t idx = 0; idx < header_phis.size(); idx++) { Phi* p = header_phis[idx]; Phi* pm = main_phis[idx]; pm->arg[1] = remap_tmp(phi_arg_for_pred(p, L.latch), copies[static_cast(F - 1)].tmp_map); pm->blk[1] = last_latch; } // main_head guard: adj_bound = bound - (F-1)*step; cmp(i_main, adj) main_head->ins = static_cast(vnew(ctx, 0, sizeof(Ins), PFn)); main_head->nins = 0; int kls = (ind.cmp_op == Ocsltl || ind.cmp_op == Ocslel) ? Kl : Kw; int64_t C = static_cast(F - 1) * ind.step; Ref adj_ref; if (ind.bound_const) { adj_ref = getcon(ctx, ind.bound - C, fn); } else { Ref adj_tmp = newtmp(ctx, "u", kls, fn); Ins sub; sub.op = Osub; sub.cls = static_cast(kls) & 3u; sub.to = adj_tmp; sub.arg[0] = ind.bound_ref; sub.arg[1] = getcon(ctx, C, fn); addins(ctx, &main_head->ins, &main_head->nins, &sub); adj_ref = adj_tmp; } Ref iv_main = head_to_main.at(ind.iv_ref.val); Ref gcmp = newtmp(ctx, "u", Kw, fn); Ins cmpi; cmpi.op = static_cast(ind.cmp_op); cmpi.cls = static_cast(ind.cmp_cls) & 3u; cmpi.to = gcmp; cmpi.arg[0] = iv_main; cmpi.arg[1] = adj_ref; addins(ctx, &main_head->ins, &main_head->nins, &cmpi); main_head->jmp.type = Jjnz; main_head->jmp.arg = gcmp; main_head->s1 = copies[0].first_blk; main_head->s2 = L.header; // remainder loop // preheader -> main_head if (L.preheader->s1 == L.header) L.preheader->s1 = main_head; if (L.preheader->s2 == L.header) L.preheader->s2 = main_head; // Remainder header phi's preheader edge: replace with main_head, value is pm.to for (size_t idx = 0; idx < header_phis.size(); idx++) { Phi* p = header_phis[idx]; Phi* pm = main_phis[idx]; for (uint kk = 0; kk < p->narg; kk++) { if (p->blk[kk] == L.preheader) { p->blk[kk] = main_head; p->arg[kk] = pm->to; } } } // link chain: preheader -> main_head -> clones -> original preheader.link Blk* saved_link = L.preheader->link; L.preheader->link = main_head; Blk** plink = &main_head->link; for (int k = 0; k < F; k++) { for (Blk* b : L.body) { Blk* nb = copies[static_cast(k)].blk_map.at(b->id); *plink = nb; plink = &nb->link; } } *plink = saved_link; if (debug['U']) { FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; fprintf(out, "> unroll partial F=%d in %s (header b%u, body=%zu, runtime_bound=%d)\n", F, fn->name, L.header->id, L.body.size(), ind.bound_const ? 0 : 1); } return main_head; } // Pass driver bool unroll_pass(Ctx* ctx, Fn* fn, std::unordered_set& unrolled) { auto loops = find_simple_loops(fn); bool any = false; for (auto& L : loops) { // Prevent infinite partial unrolling by skipping already processed main_heads. if (unrolled.count(L.header)) continue; Induction ind; if (!detect_induction(fn, L, ind)) continue; int64_t N = ind.trip; int body_ins = count_body_ins(L); // Prioritize full unroll: trip known and <= kFullUnrollTripMax, and N*body within budget if (N >= 1 && N <= kFullUnrollTripMax && static_cast(body_ins) * N <= kFullUnrollBudget) { full_unroll(ctx, fn, L, N); any = true; break; } if (N == 0) continue; // Loop doesn't execute, leave to simplcfg // Otherwise try partial unroll: N=-1 (runtime bound) or N > kFullUnrollTripMax // Skip if body is too large (F*body limit kPartialMaxBodyIns*F = 256 ins). if (body_ins <= 0 || body_ins > kPartialMaxBodyIns) continue; Blk* mh = partial_unroll(ctx, fn, L, ind, kPartialFactor); unrolled.insert(mh); any = true; break; } return any; } } // namespace void unroll(Ctx* ctx, Fn* fn) { // Caller guarantees fillcfg + filldom + filluse are ready. std::unordered_set unrolled; // main_heads already partial-unrolled, persistent across fixpoints int iter = 0; for (; iter < kUnrollFixpointIters; iter++) { if (!unroll_pass(ctx, fn, unrolled)) break; // CFG modified, rebuild for next round. fillcfg(ctx, fn); filluse(ctx, fn); filldom(fn); } if (debug['U'] && iter > 0) { FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; fprintf(out, "> unroll %s: %d round%s\n", fn->name, iter, iter == 1 ? "" : "s"); } } } // namespace rcc