/* 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 reduction_unroll.cc
/// @brief Identifies inner loop integer reductions and unrolls them into K-way parallel
/// accumulators.
///
/// See docs/reduction_unroll.md for architectural notes.
#include "rcc/all.h"
#include "rcc/loopinfo.h"
#include
#include
#include
#include
namespace rcc {
namespace {
constexpr int kUnrollK = 4;
int load_bytes(int op, int cls) {
switch (op) {
case Oloadsb:
case Oloadub:
return 1;
case Oloadsh:
case Oloaduh:
return 2;
case Oloadsw:
case Oloaduw:
return 4;
case Oload:
return (cls == Kl || cls == Kd) ? 8 : 4;
default:
return 0;
}
}
bool is_int_reduce_op(int op) {
return op == Oadd || op == Omul || op == Oand || op == Oor || op == Oxor;
}
int64_t identity_for(int op) {
switch (op) {
case Oadd:
case Oor:
case Oxor:
return 0;
case Omul:
return 1;
case Oand:
return -1;
default:
return 0;
}
}
bool is_int_cls(int cls) {
return cls == Kw || cls == Kl;
}
struct Match {
PointerIV iv_p; // pointer IV
Phi* phi_p; // pointer phi
Phi* phi_s; // accumulator phi
Ref s_init; // accumulator init (preheader side)
Ref s_next; // accumulator next (latch side, = s_op.to)
Ins* load_ins; // load inside body
Ins* op_ins; // reduce op inside body (s.next = OP s, v)
Ins* p_inc_ins; // pointer increment inside body
Ref end; // end ref for exit cmp
Blk* after; // exit block
int op; // op_ins->op
int s_cls; // accumulator and op cls
int load_op; // load_ins->op
int load_cls; // load_ins->cls
};
// Check latch body.
bool scan_body(Fn* /*fn*/, const Loop& L, const PointerIV& iv_p, Phi* phi_s, Match* m) {
m->load_ins = nullptr;
m->op_ins = nullptr;
m->p_inc_ins = nullptr;
Ref p = iv_p.phi_to;
Ref s = phi_s->to;
Ref s_next = Ref::none(), p_next = iv_p.next;
for (uint k = 0; k < phi_s->narg; k++)
if (phi_s->blk[k] == L.latch)
s_next = phi_s->arg[k];
if (rtype(s_next) != RTmp)
return false;
for (Blk* bb : L.body) {
for (uint k = 0; k < bb->nins; k++) {
Ins* i = &bb->ins[k];
int op = i->op;
// header allows exit cmp (guaranteed unique by check_exit_cmp)
if (bb == L.header && req(i->to, L.header->jmp.arg))
continue;
if (op == Onop)
continue;
if (op == Ocall || op == Oalloc4 || op == Oalloc8 || op == Oalloc16 || op == Ovaarg ||
op == Ovastart || op == Odbgloc || op == Osel0 || op == Osel1 || op == Oblit0 ||
op == Oblit1 || op == Osalloc || op == Oudiv || op == Ourem || op == Odiv ||
op == Orem)
return false;
// pointer increment?
if (req(i->to, p_next)) {
if (m->p_inc_ins)
return false;
m->p_inc_ins = i;
continue;
}
// accumulator increment?
if (req(i->to, s_next)) {
if (m->op_ins)
return false;
if (!is_int_reduce_op(op))
return false;
if (!is_int_cls(static_cast(i->cls)))
return false;
// Must be OP(s, v) or OP(v, s)
if (!req(i->arg[0], s) && !req(i->arg[1], s))
return false;
m->op_ins = i;
continue;
}
// Is it a load?
int lb = load_bytes(op, static_cast(i->cls));
if (lb > 0) {
if (m->load_ins)
return false;
if (!req(i->arg[0], p))
return false;
m->load_ins = i;
continue;
}
// Reject any other ins (reduction form requires a clean body)
return false;
}
}
if (!m->load_ins || !m->op_ins || !m->p_inc_ins)
return false;
// Loaded value must equal the other operand of the OP
Ref v_ref = m->load_ins->to;
if (!(req(m->op_ins->arg[0], v_ref) || req(m->op_ins->arg[1], v_ref)))
return false;
// class & width
int lb = load_bytes(m->load_ins->op, static_cast(m->load_ins->cls));
if (lb != iv_p.step)
return false;
if (m->op_ins->cls != m->load_ins->cls)
return false;
m->s_next = s_next;
return true;
}
bool try_match_loop(Fn* fn, const Loop& L, Match* m) {
if (L.body.size() > 2)
return false;
if (!L.preheader)
return false;
if (L.preheader->jmp.type != Jjmp)
return false;
if (L.preheader->s1 != L.header || L.preheader->s2 != nullptr)
return false;
// Collect header phis.
PointerIV iv_p{};
Phi* phi_p = nullptr;
Phi* phi_s = nullptr;
int n_ptr = 0, n_acc = 0;
for (Phi* ph = L.header->phi; ph; ph = ph->link) {
PointerIV cand{};
if (pick_pointer_iv(fn, L, ph, &cand)) {
iv_p = cand;
phi_p = ph;
n_ptr++;
continue;
}
if (ph->narg == 2 && is_int_cls(static_cast(ph->cls))) {
phi_s = ph;
n_acc++;
continue;
}
return false; // Reject any third type of phi
}
if (n_ptr != 1 || n_acc != 1)
return false;
// accumulator phi incoming edges
Ref s_init = Ref::none();
for (uint k = 0; k < phi_s->narg; k++) {
if (phi_s->blk[k] == L.preheader)
s_init = phi_s->arg[k];
else if (phi_s->blk[k] != L.latch)
return false;
}
if (rtype(s_init) == -1)
return false;
Ref end_ref;
Blk* after = nullptr;
if (!check_exit_cmp(fn, L, iv_p.phi_to, &end_ref, &after))
return false;
if (!scan_body(fn, L, iv_p, phi_s, m))
return false;
// accumulator cls == op cls, must match load cls (same bitwidth)
if (phi_s->cls != m->op_ins->cls)
return false;
if (has_outside_use(fn, L)) {
// Check outside loop uses strictly.
std::unordered_set loop_def;
for (Blk* bb : L.body) {
for (Phi* ph = bb->phi; ph; ph = ph->link)
if (rtype(ph->to) == RTmp)
loop_def.insert(ph->to.val);
for (uint k = 0; k < bb->nins; k++)
if (rtype(bb->ins[k].to) == RTmp)
loop_def.insert(bb->ins[k].to.val);
}
loop_def.erase(phi_p->to.val);
loop_def.erase(phi_s->to.val);
for (Blk* b = fn->start; b; b = b->link) {
if (L.body_bid.count(b->id))
continue;
for (Phi* ph = b->phi; ph; ph = ph->link)
for (uint k = 0; k < ph->narg; k++)
if (rtype(ph->arg[k]) == RTmp && loop_def.count(ph->arg[k].val))
return false;
for (uint k = 0; k < b->nins; k++) {
Ins* i = &b->ins[k];
for (int a = 0; a < 2; a++)
if (rtype(i->arg[a]) == RTmp && loop_def.count(i->arg[a].val))
return false;
}
if (rtype(b->jmp.arg) == RTmp && loop_def.count(b->jmp.arg.val))
return false;
if (rtype(b->jmp.arg2) == RTmp && loop_def.count(b->jmp.arg2.val))
return false;
}
}
m->iv_p = iv_p;
m->phi_p = phi_p;
m->phi_s = phi_s;
m->s_init = s_init;
m->end = end_ref;
m->after = after;
m->op = m->op_ins->op;
m->s_cls = m->op_ins->cls;
m->load_op = m->load_ins->op;
m->load_cls = static_cast(m->load_ins->cls);
return true;
}
Phi* make_phi(Ctx* ctx, int cls, Ref to, Ref arg_pre, Blk* pre, Ref arg_back, Blk* back) {
Phi* p = static_cast(alloc(ctx, sizeof(Phi)));
p->cls = static_cast(cls);
p->visit = 0;
p->to = to;
p->narg = 2;
p->arg = static_cast[(vnew(ctx, 2, sizeof(Ref), PFn));
p->blk = static_cast(vnew(ctx, 2, sizeof(Blk*), PFn));
p->arg[0] = arg_pre;
p->blk[0] = pre;
p->arg[1] = arg_back;
p->blk[1] = back;
p->link = nullptr;
return p;
}
static int rd_seq = 0;
void execute(Ctx* ctx, Fn* fn, const Loop& L, const Match& m) {
int K = kUnrollK;
int S = static_cast(m.iv_p.step);
int KS = K * S;
int my_seq = rd_seq++;
Blk* pre = L.preheader;
Blk* tail_hdr = L.header;
int s_cls = m.s_cls;
int load_op = m.load_op;
int load_cls = m.load_cls;
int op = m.op;
// Append size / size_main / end_main to the end of preheader
Ref size_ref = newtmp(ctx, "rd.size", Kl, fn);
Ref size_main_ref = newtmp(ctx, "rd.szm", Kl, fn);
Ref end_main_ref = newtmp(ctx, "rd.endm", Kl, fn);
Ins sub_i{};
sub_i.op = Osub;
sub_i.cls = Kl;
sub_i.to = size_ref;
sub_i.arg[0] = m.end;
sub_i.arg[1] = m.iv_p.base;
addins(ctx, &pre->ins, &pre->nins, &sub_i);
Ins and_i{};
and_i.op = Oand;
and_i.cls = Kl;
and_i.to = size_main_ref;
and_i.arg[0] = size_ref;
and_i.arg[1] = getcon(ctx, -static_cast(KS), fn);
addins(ctx, &pre->ins, &pre->nins, &and_i);
Ins add_i{};
add_i.op = Oadd;
add_i.cls = Kl;
add_i.to = end_main_ref;
add_i.arg[0] = m.iv_p.base;
add_i.arg[1] = size_main_ref;
addins(ctx, &pre->ins, &pre->nins, &add_i);
// Create main_header / main_body / main_join
Blk* mh = newblk(ctx);
mh->name = strf(ctx, PFn, "rd%d.mh", my_seq);
mh->id = fn->nblk++;
Blk* mb = newblk(ctx);
mb->name = strf(ctx, PFn, "rd%d.mb", my_seq);
mb->id = fn->nblk++;
Blk* mj = newblk(ctx);
mj->name = strf(ctx, PFn, "rd%d.mj", my_seq);
mj->id = fn->nblk++;
// Pre-generate names (main_header phi.to, etc.)
Ref pm_ref = newtmp(ctx, "rd.pm", Kl, fn);
Ref pm_next_ref = newtmp(ctx, "rd.pm.n", Kl, fn);
std::vector][ s_refs(static_cast(K)), s_next_refs(static_cast(K));
for (size_t i = 0; i < static_cast(K); i++) {
s_refs[i] = newtmp(ctx, "rd.s", s_cls, fn);
s_next_refs[i] = newtmp(ctx, "rd.s.n", s_cls, fn);
}
// Identity ref
Ref ident_ref = getcon(ctx, identity_for(op), fn);
// main_header phi chain: pm + s0..K-1
Phi* phi_pm = make_phi(ctx, Kl, pm_ref, m.iv_p.base, pre, pm_next_ref, mb);
mh->phi = phi_pm;
Phi* tail_phi = phi_pm;
for (size_t i = 0; i < static_cast(K); i++) {
Ref init = (i == 0) ? m.s_init : ident_ref;
Phi* ph = make_phi(ctx, s_cls, s_refs[i], init, pre, s_next_refs[i], mb);
tail_phi->link = ph;
tail_phi = ph;
}
// main_header cmp + jnz
Ref cmpm_ref = newtmp(ctx, "rd.cmpm", Kw, fn);
Ins cmpm_i{};
cmpm_i.op = Ocultl;
cmpm_i.cls = Kw;
cmpm_i.to = cmpm_ref;
cmpm_i.arg[0] = pm_ref;
cmpm_i.arg[1] = end_main_ref;
addins(ctx, &mh->ins, &mh->nins, &cmpm_i);
mh->jmp.type = Jjnz;
mh->jmp.arg = cmpm_ref;
mh->jmp.arg2 = Ref::none();
mh->s1 = mb;
mh->s2 = mj;
// main_body: K loads + K ops + 1 pointer step
std::vector][ v_refs(static_cast(K));
for (size_t i = 0; i < static_cast(K); i++) {
Ref addr;
if (i == 0) {
addr = pm_ref;
} else {
addr = newtmp(ctx, "rd.adr", Kl, fn);
Ins ai{};
ai.op = Oadd;
ai.cls = Kl;
ai.to = addr;
ai.arg[0] = pm_ref;
ai.arg[1] = getcon(ctx, static_cast(i) * S, fn);
addins(ctx, &mb->ins, &mb->nins, &ai);
}
v_refs[i] = newtmp(ctx, "rd.v", load_cls, fn);
Ins li{};
li.op = static_cast(load_op);
li.cls = static_cast(load_cls) & 3u;
li.to = v_refs[i];
li.arg[0] = addr;
li.arg[1] = Ref::none();
addins(ctx, &mb->ins, &mb->nins, &li);
}
for (size_t i = 0; i < static_cast(K); i++) {
Ins oi{};
oi.op = static_cast(op);
oi.cls = static_cast(s_cls) & 3u;
oi.to = s_next_refs[i];
oi.arg[0] = s_refs[i];
oi.arg[1] = v_refs[i];
addins(ctx, &mb->ins, &mb->nins, &oi);
}
Ins pn_i{};
pn_i.op = Oadd;
pn_i.cls = Kl;
pn_i.to = pm_next_ref;
pn_i.arg[0] = pm_ref;
pn_i.arg[1] = getcon(ctx, static_cast(KS), fn);
addins(ctx, &mb->ins, &mb->nins, &pn_i);
mb->jmp.type = Jjmp;
mb->jmp.arg = Ref::none();
mb->jmp.arg2 = Ref::none();
mb->s1 = mh;
mb->s2 = nullptr;
// main_join: log2(K) tree reduction
Ref s01_ref = newtmp(ctx, "rd.s01", s_cls, fn);
Ref s23_ref = newtmp(ctx, "rd.s23", s_cls, fn);
Ref s_main_ref = newtmp(ctx, "rd.sm", s_cls, fn);
Ins j1{};
j1.op = static_cast(op);
j1.cls = static_cast(s_cls) & 3u;
j1.to = s01_ref;
j1.arg[0] = s_refs[0];
j1.arg[1] = s_refs[1];
addins(ctx, &mj->ins, &mj->nins, &j1);
Ins j2{};
j2.op = static_cast(op);
j2.cls = static_cast(s_cls) & 3u;
j2.to = s23_ref;
j2.arg[0] = s_refs[2];
j2.arg[1] = s_refs[3];
addins(ctx, &mj->ins, &mj->nins, &j2);
Ins j3{};
j3.op = static_cast(op);
j3.cls = static_cast(s_cls) & 3u;
j3.to = s_main_ref;
j3.arg[0] = s01_ref;
j3.arg[1] = s23_ref;
addins(ctx, &mj->ins, &mj->nins, &j3);
mj->jmp.type = Jjmp;
mj->jmp.arg = Ref::none();
mj->jmp.arg2 = Ref::none();
mj->s1 = tail_hdr;
mj->s2 = nullptr;
// Change preheader.s1 -> main_header
pre->s1 = mh;
// Link new blocks.
mj->link = pre->link;
mb->link = mj;
mh->link = mb;
pre->link = mh;
// Update tail_header phis.
for (Phi* ph = tail_hdr->phi; ph; ph = ph->link) {
for (uint k = 0; k < ph->narg; k++) {
if (ph->blk[k] != pre)
continue;
ph->blk[k] = mj;
if (req(ph->to, m.phi_p->to))
ph->arg[k] = pm_ref;
else if (req(ph->to, m.phi_s->to))
ph->arg[k] = s_main_ref;
break;
}
}
}
} // namespace
void reduction_unroll(Ctx* ctx, Fn* fn) {
std::vector loops;
find_loops(fn, loops);
if (loops.empty())
return;
// Inner loops first (smaller body first)
std::sort(loops.begin(), loops.end(),
[](const Loop& a, const Loop& b) { return a.body.size() < b.body.size(); });
std::unordered_set consumed;
for (const Loop& L : loops) {
bool clash = false;
for (Blk* bb : L.body)
if (consumed.count(bb->id)) {
clash = true;
break;
}
if (clash)
continue;
Match m{};
if (!try_match_loop(fn, L, &m))
continue;
execute(ctx, fn, L, m);
for (Blk* bb : L.body)
consumed.insert(bb->id);
}
}
} // namespace rcc
]