use rucc_cost::{AddrMode, Cost, CostTable, Cycles, RegClass, Width, heuristics};
use rucc_ir::{Flags, Func, Inst, Opcode, Value};
use crate::loops::{LoopId, Loops};
use crate::machine::Machine;
use crate::scev::{Chrec, Count, Evolution, Invariant, Scev};
use crate::{Analyses, Fuel, Pass, Preserved, Stats};
const NO_TARGET: &str =
"left alone, nobody has priced this machine and the answer is a fact about the machine";
const POPULATION: &str = "loop with at least one induction variable use in it";
const USE_ADDRESS: &str = "address of a read or a write that moves by a fixed step";
const USE_COMPARE: &str = "comparison against something that moves by a fixed step";
const USE_GENERIC: &str = "other use of something that moves by a fixed step";
const GROUPED: &str = "address uses sharing one variable, apart in a constant offset";
const CANDIDATE: &str = "induction variable considered for the loop to keep";
const CHOSEN: &str = "induction variable chosen for the loop to keep";
const KEPT: &str = "loop whose own induction variables are the ones worth keeping";
const CHANGED: &str = "loop that would be cheaper with a different set of induction variables";
const TOO_MANY_USES: &str = "left alone, more uses in it than the search is allowed to look at";
const PRUNED: &str =
"candidate dropped before the search, no use wants it and it is not the loop's";
#[derive(Debug)]
pub struct Ivopts;
impl Pass for Ivopts {
fn name(&self) -> &'static str {
"ivopts"
}
fn describe(&self) -> &'static str {
"chooses the induction variables a loop should keep, and rewrites nothing"
}
fn preserves(&self) -> Preserved {
Preserved::ALL
}
fn run(&self, func: &mut Func, an: &mut Analyses, _fuel: &mut Fuel) -> Stats {
let mut stats = Stats::new();
if func.entry().is_none() {
return stats;
}
let machine = an.machine();
let Some(table) = machine.table() else {
stats.missed(NO_TARGET);
return stats;
};
let cfg = an.cfg(func).clone();
let loops = an.loops(func).clone();
let mut scev = Scev::new(func, &cfg, &loops);
for id in loops.all() {
consider(func, &loops, &mut scev, machine, table, id, &mut stats);
}
stats
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Kind {
Address,
Compare,
Generic,
}
impl Kind {
const fn remark(self) -> &'static str {
match self {
Self::Address => USE_ADDRESS,
Self::Compare => USE_COMPARE,
Self::Generic => USE_GENERIC,
}
}
}
#[derive(Clone, Copy, Debug)]
struct Want {
kind: Kind,
chrec: Chrec,
}
#[derive(Debug)]
struct Group {
kind: Kind,
chrec: Chrec,
offsets: Vec<i128>,
}
impl Group {
fn takes(&self, want: &Want) -> bool {
self.kind == Kind::Address
&& want.kind == Kind::Address
&& self.chrec.ty == want.chrec.ty
&& self.chrec.step == want.chrec.step
&& self.chrec.base.value == want.chrec.base.value
&& self.chrec.base.scale == want.chrec.base.scale
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Origin {
Original,
Derived,
Countdown,
}
#[derive(Clone, Copy, Debug)]
struct Cand {
chrec: Chrec,
origin: Origin,
}
impl Cand {
fn same(&self, other: &Self) -> bool {
self.chrec.ty == other.chrec.ty
&& self.chrec.base == other.chrec.base
&& self.chrec.step == other.chrec.step
}
}
fn consider(
func: &Func,
loops: &Loops,
scev: &mut Scev<'_>,
machine: Machine,
table: &CostTable,
id: LoopId,
stats: &mut Stats,
) {
let wants = collect(func, loops, scev, id);
if wants.is_empty() {
return;
}
stats.note(POPULATION);
for want in &wants {
stats.note(want.kind.remark());
}
if wants.len() > heuristics::IV_MAX_CONSIDERED_USES {
stats.missed(TOO_MANY_USES);
return;
}
let groups = group(wants);
for one in &groups {
if one.offsets.len() > 1 {
stats.note(GROUPED);
}
}
let mut cands = candidates(func, loops, scev, id, &groups);
prune(&mut cands, table, &groups, stats);
for _ in &cands {
stats.note(CANDIDATE);
}
let room = machine.allocatable(RegClass::Integer).unwrap_or(0);
let chosen = select(table, &groups, &cands, room);
for _ in &chosen {
stats.note(CHOSEN);
}
let untouched = chosen.iter().all(|&at| cands[at].origin == Origin::Original)
&& chosen.len() == cands.iter().filter(|c| c.origin == Origin::Original).count();
stats.note(if untouched { KEPT } else { CHANGED });
}
fn collect(func: &Func, loops: &Loops, scev: &mut Scev<'_>, id: LoopId) -> Vec<Want> {
let mut wants = Vec::new();
for &block in loops.blocks(id) {
if loops.innermost(block) != Some(id) {
continue;
}
for inst in func.insts(block) {
let data = func[inst];
if data.opcode.is_terminator() {
continue;
}
if moves(func, scev, id, inst) {
continue;
}
let args = &func[data.args];
for (position, &arg) in args.iter().enumerate() {
let Some(chrec) = affine(scev, id, arg) else { continue };
let kind = classify(data.opcode, position);
wants.push(Want { kind, chrec });
}
}
}
wants
}
fn moves(func: &Func, scev: &mut Scev<'_>, id: LoopId, inst: Inst) -> bool {
func[inst].results().any(|result| affine(scev, id, result).is_some())
}
fn affine(scev: &mut Scev<'_>, id: LoopId, value: Value) -> Option<Chrec> {
match scev.evolution(id, value) {
Evolution::Affine(chrec) if !chrec.step.is_zero() => Some(chrec),
_ => None,
}
}
fn classify(opcode: Opcode, position: usize) -> Kind {
match (opcode, position) {
(Opcode::Load, 0) | (Opcode::Store, 1) => Kind::Address,
(Opcode::ICmp, _) => Kind::Compare,
_ => Kind::Generic,
}
}
fn group(wants: Vec<Want>) -> Vec<Group> {
let mut groups: Vec<Group> = Vec::new();
for want in wants {
match groups.iter_mut().find(|one| one.takes(&want)) {
Some(one) => one.offsets.push(want.chrec.base.offset - one.chrec.base.offset),
None => groups.push(Group { kind: want.kind, chrec: want.chrec, offsets: vec![0] }),
}
}
groups
}
fn candidates(
func: &Func,
loops: &Loops,
scev: &mut Scev<'_>,
id: LoopId,
groups: &[Group],
) -> Vec<Cand> {
let mut cands: Vec<Cand> = Vec::new();
let mut add = |cand: Cand| {
if !cands.iter().any(|had| had.same(&cand)) {
cands.push(cand);
}
};
for at in 0..func[loops.header(id)].params.len() {
let param = func[loops.header(id)].params[at];
if let Some(chrec) = affine(scev, id, param) {
add(Cand { chrec, origin: Origin::Original });
}
}
for one in groups {
add(Cand { chrec: one.chrec, origin: Origin::Derived });
}
if let Some(bound) = scev.bound(id) {
if let Some(count) = bound.under_undefined_overflow() {
if let Some(chrec) = countdown(count, groups) {
add(Cand { chrec, origin: Origin::Countdown });
}
}
}
cands
}
fn countdown(count: Count, groups: &[Group]) -> Option<Chrec> {
let Count::Exact(iterations) = count else { return None };
let iterations = i128::try_from(iterations).ok()?;
let ty = groups.iter().map(|one| one.chrec.ty).find(|ty| ty.is_int())?;
Some(Chrec {
base: Invariant::number(iterations),
step: Invariant::number(-1),
ty,
flags: Flags::NONE,
})
}
fn prune(cands: &mut Vec<Cand>, table: &CostTable, groups: &[Group], stats: &mut Stats) {
if cands.len() <= heuristics::IV_ALWAYS_PRUNE_CAND_SET_BOUND {
return;
}
let mut wanted = vec![false; cands.len()];
for one in groups {
let best = (0..cands.len())
.filter(|&at| !serve(table, one, &cands[at]).is_infinite())
.min_by_key(|&at| serve(table, one, &cands[at]));
if let Some(at) = best {
wanted[at] = true;
}
}
let mut at = 0;
cands.retain(|cand| {
let keep = wanted[at] || cand.origin == Origin::Original;
at += 1;
if !keep {
stats.note(PRUNED);
}
keep
});
}
fn serve(table: &CostTable, group: &Group, cand: &Cand) -> Cost {
if group.chrec.ty != cand.chrec.ty {
return Cost::INFINITE;
}
let Some(scale) = ratio(cand.chrec.step, group.chrec.step) else { return Cost::INFINITE };
let scaled = match cand.chrec.base.times(Invariant::number(scale)) {
Some(scaled) => scaled,
None => return Cost::INFINITE,
};
let Some(rest) = group.chrec.base.minus(scaled) else { return Cost::INFINITE };
match group.kind {
Kind::Address => address_cost(table, scale, rest),
Kind::Compare | Kind::Generic => value_cost(table, group.chrec.ty, scale, rest),
}
}
fn ratio(step: Invariant, wanted: Invariant) -> Option<i128> {
let (step, wanted) = (step.as_number()?, wanted.as_number()?);
if step == 0 || wanted % step != 0 {
return None;
}
Some(wanted / step)
}
fn address_cost(table: &CostTable, scale: i128, rest: Invariant) -> Cost {
let indexed = scale != 1;
let displaced = rest.offset != 0;
let symbolic = rest.value.is_some() && rest.scale != 0;
if indexed && !legal_scale(scale) {
let mult = table.mult_of(width(rest)).max(Cycles::ONE);
return Cost::cycles(mult) + address_cost(table, 1, rest);
}
let mode = match (indexed, symbolic, displaced) {
(false, false, false) => AddrMode::Base,
(false, false, true) => AddrMode::BaseDisp,
(false, true, false) => AddrMode::BaseIndex,
(true, false, false) => AddrMode::BaseIndexScale,
(true, false, true) => AddrMode::BaseIndexScaleDisp,
(_, true, _) => {
let left = Invariant::number(rest.offset);
return Cost::cycles(table.add) + address_cost(table, scale, left);
}
};
let priced = table.addr_cost(mode);
if priced.is_infinite() {
let parts = i64::from(mode.complexity());
return Cost::cycles(table.lea) * parts + table.addr_cost(AddrMode::Base);
}
priced
}
fn value_cost(table: &CostTable, ty: rucc_ir::Type, scale: i128, rest: Invariant) -> Cost {
let mut cost = Cost::ZERO;
if scale != 1 {
let bits = ty.bits();
let mult = match Width::from_bits(bits) {
Some(width) if scale <= 0 || !scale.unsigned_abs().is_power_of_two() => {
table.mult[width.index()]
}
Some(_) => table.shift_const,
None => return Cost::INFINITE,
};
cost += Cost::cycles(mult);
}
if rest.offset != 0 || (rest.value.is_some() && rest.scale != 0) {
cost += Cost::cycles(table.add);
}
cost
}
fn legal_scale(scale: i128) -> bool {
matches!(scale, 1 | 2 | 4 | 8)
}
fn width(_rest: Invariant) -> Width {
Width::W64
}
fn upkeep(table: &CostTable, cand: &Cand) -> Cost {
let step = Cost::cycles(table.add);
match cand.origin {
Origin::Original => step,
Origin::Derived | Origin::Countdown => {
step + Cost::cycles(Cycles::ONE * i64::from(heuristics::IVOPTS_NEW_VARIABLE_BIAS))
}
}
}
fn total(
table: &CostTable,
groups: &[Group],
cands: &[Cand],
set: &[usize],
room: u32,
) -> Option<Cost> {
let mut cost = Cost::ZERO;
for group in groups {
let best = set.iter().map(|&at| serve(table, group, &cands[at])).min()?;
if best.is_infinite() {
return None;
}
cost += best * i64::try_from(group.offsets.len()).unwrap_or(1);
}
for &at in set {
cost += upkeep(table, &cands[at]);
}
let spare = room.saturating_sub(heuristics::LOOP_RESERVED_REGS);
let over = u32::try_from(set.len()).unwrap_or(u32::MAX).saturating_sub(spare);
cost += Cost::cycles(Cycles::ONE * i64::from(over) * i64::from(heuristics::IVOPTS_SET_PENALTY));
Some(cost)
}
fn select(table: &CostTable, groups: &[Group], cands: &[Cand], room: u32) -> Vec<usize> {
let mut set: Vec<usize> =
(0..cands.len()).filter(|&at| cands[at].origin == Origin::Original).collect();
for group in groups {
let served = set.iter().any(|&at| !serve(table, group, &cands[at]).is_infinite());
if served {
continue;
}
let own = (0..cands.len()).find(|&at| {
cands[at].origin == Origin::Derived && cands[at].chrec.base == group.chrec.base
});
if let Some(at) = own {
set.push(at);
}
}
set.sort_unstable();
set.dedup();
let Some(mut best) = total(table, groups, cands, &set, room) else { return set };
loop {
let mut moved = None;
for at in 0..cands.len() {
let mut tried = set.clone();
match tried.iter().position(|&had| had == at) {
Some(there) => {
tried.remove(there);
}
None => tried.push(at),
}
let Some(cost) = total(table, groups, cands, &tried, room) else { continue };
if cost < best {
best = cost;
moved = Some(tried);
}
}
match moved {
Some(tried) => set = tried,
None => return set,
}
}
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Block, Builder, Flags, Func, IntPred, MemInfo, MemOrder, Opcode, Restrict, Signature, Type,
Value,
};
use super::{
CANDIDATE, CHANGED, CHOSEN, GROUPED, Ivopts, KEPT, NO_TARGET, POPULATION, USE_ADDRESS,
USE_COMPARE, USE_GENERIC,
};
use crate::stats::Kind;
use crate::{Analyses, Fuel, Pass, Stats};
fn choose(func: &mut Func) -> Stats {
Ivopts.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
}
fn plain() -> MemInfo {
MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
}
}
struct Counted {
head: Block,
body: Block,
out: Block,
counter: Value,
}
fn counted(func: &mut Func, into: Block, limit: i128) -> Counted {
let head = func.create_block();
let body = func.create_block();
let out = func.create_block();
let i = func.append_param(head, Type::int(64));
let carried = func.append_param(body, Type::int(64));
let mut build = Builder::new(func, into);
let zero = build.iconst(Type::int(64), 0);
build.jump(head, &[zero]);
let mut build = Builder::new(func, head);
let stop = build.iconst(Type::int(64), limit);
let test = build.icmp(IntPred::Slt, i, stop);
build.br_if(test, body, &[i], out, &[]);
Counted { head, body, out, counter: carried }
}
fn close(func: &mut Func, it: &Counted, at: Block) {
let mut build = Builder::new(func, at);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, it.counter, one, Flags::NSW);
build.jump(it.head, &[next]);
}
fn shell(names: &mut Interner) -> (Func, Block, Value) {
let signature = Signature::new().with_params(&[Type::PTR]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let base = func.append_param(entry, Type::PTR);
(func, entry, base)
}
fn element(build: &mut Builder<'_>, base: Value, counter: Value, away: i128) -> Value {
let four = build.iconst(Type::int(64), 4);
let by = build.binary(Opcode::Mul, counter, four, Flags::NSW);
let at = build.binary(Opcode::PtrAdd, base, by, Flags::NONE);
if away == 0 {
return at;
}
let past = build.iconst(Type::int(64), away * 4);
build.binary(Opcode::PtrAdd, at, past, Flags::NONE)
}
#[test]
fn a_loop_walking_one_array_has_one_address_use_and_one_comparison() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let addr = element(&mut build, base, it.counter, 0);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Note, POPULATION), 1);
assert_eq!(stats.count(Kind::Note, USE_ADDRESS), 1);
assert_eq!(
stats.count(Kind::Note, USE_COMPARE),
1,
"the exit test is a use of the counter"
);
assert_eq!(stats.count(Kind::Note, USE_GENERIC), 0, "the increment is the variable itself");
assert!(!stats.changed(), "the choosing rewrites nothing");
}
#[test]
fn three_accesses_a_constant_apart_are_one_group_wanting_one_variable() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let zero = build.iconst(Type::int(32), 0);
for away in [0, 1, 2] {
let addr = element(&mut build, base, it.counter, away);
build.store(zero, addr, plain(), Flags::NONE);
}
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Note, USE_ADDRESS), 3);
assert_eq!(stats.count(Kind::Note, GROUPED), 1, "one group, not three");
assert_eq!(stats.count(Kind::Note, CHOSEN), 2);
assert_eq!(stats.count(Kind::Note, CHANGED), 1);
}
#[test]
fn a_walk_of_two_arrays_takes_a_variable_it_did_not_have() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let from = element(&mut build, base, it.counter, 0);
let read = build.load(Type::int(32), from, plain(), Flags::NONE);
let into = element(&mut build, base, it.counter, 4096);
build.store(read, into, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Note, USE_ADDRESS), 2);
assert_eq!(stats.count(Kind::Note, GROUPED), 1, "a constant apart, so one group");
assert_eq!(stats.count(Kind::Note, CHOSEN), 2, "the counter and one address variable");
assert_eq!(stats.count(Kind::Note, CHANGED), 1);
}
#[test]
fn a_loop_that_only_reads_a_fixed_address_is_not_asked_about() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let read = build.load(Type::int(32), base, plain(), Flags::NONE);
build.store(read, base, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Note, USE_ADDRESS), 0);
assert_eq!(stats.count(Kind::Note, USE_COMPARE), 1);
assert_eq!(stats.count(Kind::Note, POPULATION), 1);
assert_eq!(stats.count(Kind::Note, KEPT), 1);
assert_eq!(stats.count(Kind::Note, CHOSEN), 1);
}
#[test]
fn a_machine_nobody_priced_is_told_so_rather_than_guessed_at() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let addr = element(&mut build, base, it.counter, 0);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let mut an = Analyses::new(crate::machine::Machine::unknown());
let stats = Ivopts.run(&mut func, &mut an, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Missed, NO_TARGET), 1);
assert_eq!(stats.count(Kind::Note, POPULATION), 0);
assert_eq!(stats.count(Kind::Note, CANDIDATE), 0);
}
#[test]
fn a_function_with_no_loop_in_it_is_left_entirely_alone() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let mut build = Builder::new(&mut func, entry);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, base, plain(), Flags::NONE);
build.ret(&[]);
let stats = choose(&mut func);
assert_eq!(stats.events().len(), 0);
assert!(!stats.changed());
}
}