use std::collections::{HashMap, HashSet};
use rucc_cost::heuristics;
use rucc_ir::{
Block, BlockCall, Builder, Extra, Flags, Func, Inst, InstData, IntPred, Opcode, Type, Value,
};
use crate::cfg::Cfg;
use crate::copy;
use crate::discharge::operand_of;
use crate::loops::{LoopId, Loops};
use crate::scev::{Evolution, Scev};
use crate::trip::{Around, counted, covered, inst_of};
use crate::{Analyses, Fuel, Pass, Preserved, Stats};
const SPLIT: &str = "loop split, the iterations in front of the first one that could fail a check \
run without them";
const NO_FUEL: &str = "loop left alone, the pass ran out of fuel";
const NO_PREHEADER: &str = "loop left alone, it has no block in front of it to put a check in";
const A_LOOP_INSIDE: &str = "loop left alone, it has another loop inside it";
const MANY_LATCHES: &str = "loop left alone, it goes back to its header from more than one place";
const A_CALL_INSIDE: &str = "loop left alone, a call in it might free what the loop is reading";
const NOT_COPYABLE: &str = "loop left alone, something in it carries a side table this cannot copy";
const ESCAPES: &str = "loop left alone, a value it defines is read outside it";
const TOO_BIG: &str = "loop left alone, the two halves would be more code than the limit allows";
const NOT_A_SWEEP: &str = "check kept in both halves, its address does not walk the loop by a \
constant";
const NOT_FOLLOWED: &str = "check kept in both halves, what its address does round the loop is not \
something the analysis follows";
const DOES_NOT_MOVE: &str = "check kept in both halves, its address is the same every iteration";
const BACKWARDS: &str = "check kept in both halves, its address walks the loop from high to low";
const MISALIGNED: &str =
"check kept in both halves, its step is not a whole number of its alignment";
const ALREADY_COMPUTED: &str =
"check kept in both halves, how many bytes it covers is a number only the program has";
#[derive(Debug)]
pub struct Split;
impl Pass for Split {
fn name(&self) -> &'static str {
"split"
}
fn describe(&self) -> &'static str {
"a loop becomes a run of iterations with no checks in it and the rest of the loop with them"
}
fn preserves(&self) -> Preserved {
Preserved::NONE
}
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 cfg = an.cfg(func).clone();
let loops = an.loops(func).clone();
if loops.count() == 0 {
return stats;
}
let mut plans = Vec::new();
{
let mut scev = Scev::new(func, &cfg, &loops);
for id in loops.all() {
sweep(func, &cfg, &loops, &mut scev, id, &mut plans, &mut stats);
}
}
let mut changed = false;
for plan in plans {
if !fuel.take() {
stats.missed(NO_FUEL);
continue;
}
apply(func, &plan);
stats.optimized(SPLIT);
changed = true;
}
if changed {
an.clear();
}
stats
}
}
#[derive(Debug)]
struct Sweep {
check: Inst,
base: Value,
offset: i128,
step: i128,
reach: i128,
}
#[derive(Debug)]
struct Plan {
preheader: Block,
header: Block,
latch: Block,
body: Vec<Block>,
around: Around,
sweeps: Vec<Sweep>,
}
fn sweep(
func: &Func,
cfg: &Cfg,
loops: &Loops,
scev: &mut Scev<'_>,
id: LoopId,
plans: &mut Vec<Plan>,
stats: &mut Stats,
) {
let body = loops.blocks(id).to_vec();
let checks: Vec<Inst> = body
.iter()
.flat_map(|&block| func.insts(block).collect::<Vec<Inst>>())
.filter(|&inst| matches!(func[inst].opcode, Opcode::CheckBounds | Opcode::CheckLive))
.collect();
if checks.is_empty() {
return;
}
let (preheader, latch) = match shaped(func, cfg, loops, id, &body) {
Ok(shape) => shape,
Err(why) => {
stats.missed(why);
return;
}
};
let around = match counted(scev, id) {
Ok(around) => around,
Err(why) => {
stats.missed(why);
return;
}
};
let mut sweeps = Vec::new();
for check in checks {
match walked(func, scev, id, check) {
Ok(sweep) => sweeps.push(sweep),
Err(why) => stats.missed(why),
}
}
if sweeps.is_empty() {
return;
}
plans.push(Plan { preheader, header: loops.header(id), latch, body, around, sweeps });
}
fn shaped(
func: &Func,
cfg: &Cfg,
loops: &Loops,
id: LoopId,
body: &[Block],
) -> Result<(Block, Block), &'static str> {
let Some(preheader) = loops.preheader(cfg, id) else {
return Err(NO_PREHEADER);
};
let [latch] = loops.latches(id) else {
return Err(MANY_LATCHES);
};
for &block in body {
if loops.innermost(block) != Some(id) {
return Err(A_LOOP_INSIDE);
}
for inst in func.insts(block) {
if matches!(
func[inst].opcode,
Opcode::Call
| Opcode::CallIndirect
| Opcode::TailCall
| Opcode::InlineAsm
| Opcode::MetaEnd
| Opcode::MetaTransfer
) {
return Err(A_CALL_INSIDE);
}
if !copy::copyable(func, inst) {
return Err(NOT_COPYABLE);
}
}
}
let inside: HashSet<Block> = body.iter().copied().collect();
if escapes(func, body, &inside) {
return Err(ESCAPES);
}
let size = body.iter().map(|&block| func.insts(block).count()).sum::<usize>();
if size > heuristics::SPLIT_MAX_INSNS as usize {
return Err(TOO_BIG);
}
Ok((preheader, *latch))
}
fn escapes(func: &Func, body: &[Block], inside: &HashSet<Block>) -> bool {
let mut defined: HashSet<Value> = HashSet::new();
for &block in body {
defined.extend(func[block].params.iter().copied());
for inst in func.insts(block) {
defined.extend(func[inst].results());
}
}
for block in func.blocks() {
if inside.contains(&block) {
continue;
}
for inst in func.insts(block) {
if func[func[inst].args].iter().any(|value| defined.contains(value)) {
return true;
}
for call in func.successors(inst) {
if func[call.args].iter().any(|value| defined.contains(value)) {
return true;
}
}
}
}
false
}
fn walked(
func: &Func,
scev: &mut Scev<'_>,
id: LoopId,
check: Inst,
) -> Result<Sweep, &'static str> {
let args = &func[func[check].args];
if args.len() > 2 {
return Err(ALREADY_COMPUTED);
}
let (Some(&capability), Some(&pointer)) = (args.first(), args.get(1)) else {
return Err(NOT_A_SWEEP);
};
if operand_of(func, capability, Opcode::CapOf, 0) != Some(pointer) {
return Err(NOT_A_SWEEP);
}
let (reach, align) = match func[check].extra {
Extra::Mem(held) => (i128::from(func[held].size), i128::from(func[held].align)),
_ => (1, 1),
};
let chrec = match scev.evolution(id, pointer) {
Evolution::Affine(chrec) => chrec,
Evolution::Invariant(_) => return Err(DOES_NOT_MOVE),
_ => return Err(NOT_FOLLOWED),
};
let Some(step) = chrec.step.as_number() else {
return Err(NOT_A_SWEEP);
};
if step <= 0 {
return Err(BACKWARDS);
}
let (Some(base), 1) = (chrec.base.value, chrec.base.scale) else {
return Err(NOT_A_SWEEP);
};
if step % align != 0 {
return Err(MISALIGNED);
}
Ok(Sweep { check, base, offset: chrec.base.offset, step, reach })
}
fn apply(func: &mut Func, plan: &Plan) {
let mut renamed: HashMap<Value, Value> = HashMap::new();
let copies = copy::blocks(func, &plan.body, &mut renamed);
let slow = copies[&plan.header];
let word = Type::int(64);
let types: Vec<Type> = func[plan.header].params.iter().map(|¶m| func[param].ty).collect();
let guard = func.create_block();
let round = func.append_param(guard, word);
let carried: Vec<Value> = types.iter().map(|&ty| func.append_param(guard, ty)).collect();
let (limit, start) = limited(func, plan);
let mut build = Builder::new(func, guard);
let inside = build.icmp(IntPred::Slt, round, limit);
build.br_if(inside, plan.header, &carried, slow, &carried);
let term = func.terminator(plan.preheader).expect("a preheader ends in a jump to the header");
route(func, term, plan.header, guard, start);
let term = func.terminator(plan.latch).expect("a latch ends in a branch back to the header");
let mut build = Builder::new(func, plan.latch);
let one = build.iconst(word, 1);
let next = build.binary(Opcode::Add, round, one, Flags::NSW);
for value in [one, next] {
let inst = inst_of(func, value);
func.remove_inst(inst);
func.insert_before(inst, term);
}
route(func, term, plan.header, guard, next);
for sweep in &plan.sweeps {
func.remove_inst(sweep.check);
}
}
fn route(func: &mut Func, term: Inst, from: Block, to: Block, first: Value) {
for at in func.target_list(term).iter() {
let call = func[at];
if call.block != from {
continue;
}
let mut args = vec![first];
args.extend_from_slice(&func[call.args]);
let args = func.push_values(&args);
func.set_block_call(at, BlockCall { block: to, args });
}
}
fn limited(func: &mut Func, plan: &Plan) -> (Value, Value) {
let term = func.terminator(plan.preheader).expect("a preheader ends in a jump to the header");
let mut made = Vec::new();
let mut build = Builder::new(func, plan.preheader);
let start = build.iconst(Type::int(64), 0);
made.push(start);
let mut limit: Option<Value> = None;
for sweep in &plan.sweeps {
let allows = reachable(&mut build, &mut made, sweep, plan.around);
limit = Some(match limit {
None => allows,
Some(so_far) => {
let smaller = build.icmp(IntPred::Slt, allows, so_far);
made.push(smaller);
let least = build.select(smaller, allows, so_far);
made.push(least);
least
}
});
}
let limit = limit.expect("a plan holds at least one check");
for value in made {
let inst = inst_of(func, value);
func.remove_inst(inst);
func.insert_before(inst, term);
}
(limit, start)
}
fn reachable(
build: &mut Builder<'_>,
made: &mut Vec<Value>,
sweep: &Sweep,
around: Around,
) -> Value {
let word = Type::int(64);
let first = if sweep.offset == 0 {
sweep.base
} else {
let by = build.iconst(word, sweep.offset);
made.push(by);
let args = build.func().push_values(&[sweep.base, by]);
let sum = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
made.push(sum);
sum
};
let want = match around {
Around::Number(times) => {
let far = times.saturating_mul(sweep.step).saturating_add(sweep.reach);
let far = i64::try_from(far).unwrap_or(i64::MAX);
let bytes = build.iconst(word, i128::from(far));
made.push(bytes);
bytes
}
Around::Computed(count, reading) => {
covered(build, made, count, sweep.step, sweep.reach, reading, Flags::NONE)
}
};
let args = build.func().push_values(&[first]);
let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
made.push(capability);
let args = build.func().push_values(&[capability, first, want]);
let extent = build.value(InstData { args, ..InstData::new(Opcode::CapExtent) }, word);
made.push(extent);
let reach = build.iconst(word, sweep.reach);
made.push(reach);
let left = build.binary(Opcode::Sub, extent, reach, Flags::NSW);
made.push(left);
let zero = build.iconst(word, 0);
made.push(zero);
let short = build.icmp(IntPred::Slt, left, zero);
made.push(short);
let mut steps = left;
if sweep.step != 1 {
let by = build.iconst(word, sweep.step);
made.push(by);
steps = build.binary(Opcode::SDiv, left, by, Flags::NONE);
made.push(steps);
}
let one = build.iconst(word, 1);
made.push(one);
let allows = build.binary(Opcode::Add, steps, one, Flags::NSW);
made.push(allows);
let clamped = build.select(short, zero, allows);
made.push(clamped);
clamped
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Block, Builder, Extra, Flags, Func, Inst, InstData, IntPred, MemInfo, MemOrder, Module,
Opcode, Restrict, Signature, Type, Value, verify_func,
};
use rucc_target::{TargetInfo, Triple};
use super::{SPLIT, Split};
use crate::canon::Canon;
use crate::stats::Kind;
use crate::{Fuel, Pass, Stats};
const TRIPS: i128 = 16;
const WIDTH: i128 = 4;
fn leaving() -> (Interner, Func, Vec<Block>) {
walking(Some(TRIPS))
}
fn counting() -> (Interner, Func, Vec<Block>) {
walking(None)
}
fn walking(times: Option<i128>) -> (Interner, Func, Vec<Block>) {
let mut names = Interner::new();
let mut params = vec![Type::PTR];
params.extend(times.is_none().then_some(Type::int(64)));
let mut func = Func::new(names.intern("f"), Signature::new().with_params(¶ms));
let entry = func.create_block();
let head = func.create_block();
let more = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let handed = times.is_none().then(|| func.append_param(entry, Type::int(64)));
let counter = func.append_param(head, Type::int(64));
let zero = Builder::new(&mut func, entry).iconst(Type::int(64), 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let by = build.iconst(Type::int(64), WIDTH);
let scaled = build.binary(Opcode::Mul, counter, by, Flags::NSW);
let args = build.func().push_values(&[array, scaled]);
let pointer = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
check(&mut build, pointer);
let read = build.load(Type::int(32), pointer, mem(), Flags::NONE);
let nothing = build.iconst(Type::int(32), 0);
let stop = build.icmp(IntPred::Eq, read, nothing);
build.br_if(stop, done, &[], more, &[]);
let mut build = Builder::new(&mut func, more);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
let limit = match (times, handed) {
(Some(times), _) => build.iconst(Type::int(64), times),
(None, handed) => handed.expect("a loop with no number for a limit was handed one"),
};
let again = build.icmp(IntPred::Slt, next, limit);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
(names, func, vec![entry, head, more, done])
}
fn mem() -> MemInfo {
MemInfo {
size: WIDTH as u64,
align: WIDTH as u32,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
}
}
fn check(build: &mut Builder<'_>, pointer: Value) {
let args = build.func().push_values(&[pointer]);
let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
let args = build.func().push_values(&[capability, pointer]);
let extra = Extra::Mem(build.func().add_mem(mem()));
build.inst(InstData { args, extra, ..InstData::new(Opcode::CheckBounds) }, &[]);
}
fn split_up(func: &mut Func) -> Stats {
let mut an = crate::machine::fixtures::analyses();
Canon.run(func, &mut an, &mut Fuel::unlimited());
Split.run(func, &mut an, &mut Fuel::unlimited())
}
fn all(func: &Func, opcode: Opcode) -> Vec<(Block, Inst)> {
func.blocks()
.flat_map(|block| func.insts(block).map(move |inst| (block, inst)).collect::<Vec<_>>())
.filter(|&(_, inst)| func[inst].opcode == opcode)
.collect()
}
fn sound(func: &Func, names: &mut Interner) {
let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
let module = Module::new(names.intern("t.c"), &target);
if let Err(errors) = verify_func(&module, func, names) {
panic!("{errors:#?}");
}
}
#[test]
fn a_loop_that_can_stop_early_is_split_even_though_hoisting_will_not_touch_it() {
let (mut names, mut func, _) = leaving();
let mut an = crate::machine::fixtures::analyses();
Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
let refused = crate::hoist::Hoist.run(&mut func, &mut an, &mut Fuel::unlimited());
assert!(!refused.changed(), "hoisting has nothing to say about this loop");
let stats = Split.run(&mut func, &mut an, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
sound(&func, &mut names);
}
#[test]
fn the_half_the_loop_runs_first_has_no_check_in_it_and_the_other_one_keeps_it() {
let (mut names, mut func, blocks) = leaving();
let head = blocks[1];
split_up(&mut func);
let left = all(&func, Opcode::CheckBounds);
assert_eq!(left.len(), 1, "one check, and it is the one the slow half kept");
assert_ne!(left[0].0, head, "and it is not in the block the loop started in");
sound(&func, &mut names);
}
#[test]
fn how_far_the_runtime_is_asked_to_look_is_settled_in_front_of_the_loop() {
let (mut names, mut func, _) = leaving();
split_up(&mut func);
let asked = all(&func, Opcode::CapExtent);
assert_eq!(asked.len(), 1, "one question for the one check that was sized");
let cfg = crate::Cfg::new(&func);
let doms = crate::Dominators::new(&cfg);
let loops = crate::Loops::new(&cfg, &doms);
assert!(
loops.all().all(|id| !loops.contains(id, asked[0].0)),
"and it is outside the loop"
);
sound(&func, &mut names);
}
#[test]
fn a_loop_with_a_call_in_it_is_left_alone() {
let (mut names, mut func, blocks) = leaving();
let more = blocks[2];
let term = func.terminator(more).expect("the latch branches");
let callee = names.intern("might_free");
let signature = func.add_signature(Signature::new());
let call = Builder::new(&mut func, more).call(callee, signature, &[]);
func.remove_inst(call);
func.insert_before(call, term);
let stats = split_up(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::A_CALL_INSIDE), 1);
}
#[test]
fn a_check_whose_address_does_not_move_is_left_to_hoisting() {
let (_, mut func, blocks) = leaving();
let (entry, head) = (blocks[0], blocks[1]);
let array = func[entry].params[0];
let term = func.terminator(head).expect("the header branches");
let mut build = Builder::new(&mut func, head);
check(&mut build, array);
let made: Vec<Inst> = func.insts(head).skip_while(|&inst| inst != term).skip(1).collect();
for inst in made {
func.remove_inst(inst);
func.insert_before(inst, term);
}
let stats = split_up(&mut func);
assert_eq!(stats.count(Kind::Missed, super::DOES_NOT_MOVE), 1);
assert_eq!(
all(&func, Opcode::CheckBounds).len(),
3,
"the walking check left the fast half, and the still one stayed in both"
);
}
#[test]
fn a_loop_whose_count_is_an_expression_is_split_on_what_that_expression_says() {
let (mut names, mut func, _) = counting();
let stats = split_up(&mut func);
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
assert_eq!(all(&func, Opcode::CapExtent).len(), 1);
assert_eq!(all(&func, Opcode::CheckBounds).len(), 1, "the fast half lost its check");
sound(&func, &mut names);
}
#[test]
fn the_pass_stops_when_the_fuel_runs_out() {
let (_, mut func, _) = leaving();
let mut an = crate::machine::fixtures::analyses();
Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
let stats = Split.run(&mut func, &mut an, &mut Fuel::of(0));
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
}
}