use std::collections::HashSet;
use rucc_cost::heuristics;
use rucc_ir::{Block, Func, Inst, Opcode, Value};
use crate::cfg::Cfg;
use crate::dom::{Dominators, PostDominators};
use crate::live::Liveness;
use crate::loops::{LoopId, Loops};
use crate::pressure::{Class, Pressure};
use crate::range::query::Ranges;
use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats, speculate};
const HOISTED: &str = "computation moved in front of the loop, nothing in the loop changes it";
const SPECULATIVE: &str =
"left in the loop, it does not run on every entry and working it out early could fault";
const EFFECTS: &str = "left in the loop, moving it would change what the program does";
const PRESSURE: &str = "left in the loop, it is cheaper than the register holding it would cost";
const MEMORY: &str = "left in the loop, the loop writes memory and nothing here says which memory";
const NO_PREHEADER: &str = "loop left as it was, it has not been canonicalized";
const SPINS: &str = "loop left as it was, it has no way out, so nothing in it is known to run";
const NO_FUEL: &str = "loop left as it was, the pass ran out of fuel";
#[derive(Debug)]
pub struct Licm;
pub static LICM: Licm = Licm;
impl Pass for Licm {
fn name(&self) -> &'static str {
"licm"
}
fn describe(&self) -> &'static str {
"moves a computation whose operands do not change in a loop in front of the loop"
}
fn preserves(&self) -> Preserved {
Preserved::ALL.without(Analysis::Liveness).without(Analysis::Pressure)
}
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 dom = an.dominators(func).clone();
let post = an.post_dominators(func).clone();
let invented: HashSet<Block> = post.fake_exits().iter().copied().collect();
let mut order: Vec<LoopId> = loops.all().collect();
order.sort_by_key(|&id| std::cmp::Reverse(loops.depth(id)));
let mut pressure = Pressure::of(func, &cfg, &Liveness::of(func, &cfg));
for id in order {
let job = Job { cfg: &cfg, dom: &dom, post: &post, loops: &loops, invented: &invented };
if job.run(func, &pressure, id, fuel, &mut stats) {
pressure = Pressure::of(func, &cfg, &Liveness::of(func, &cfg));
}
}
stats
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Move {
Anywhere,
IfItWasGoingToRun,
Nowhere,
}
struct Job<'a> {
cfg: &'a Cfg,
dom: &'a Dominators,
post: &'a PostDominators,
loops: &'a Loops,
invented: &'a HashSet<Block>,
}
impl Job<'_> {
fn run(
&self,
func: &mut Func,
pressure: &Pressure,
id: LoopId,
fuel: &mut Fuel,
stats: &mut Stats,
) -> bool {
let Some(preheader) = self.loops.preheader(self.cfg, id) else {
stats.missed(NO_PREHEADER);
return false;
};
let Some(landing) = func.terminator(preheader) else {
return false;
};
let plan = self.plan(func, pressure, id, preheader, fuel, stats);
for inst in &plan {
func.remove_inst(*inst);
func.insert_before(*inst, landing);
stats.optimized(HOISTED);
}
!plan.is_empty()
}
fn plan(
&self,
func: &Func,
pressure: &Pressure,
id: LoopId,
preheader: Block,
fuel: &mut Fuel,
stats: &mut Stats,
) -> Vec<Inst> {
let header = self.loops.header(id);
let inside: HashSet<Block> = self.loops.blocks(id).iter().copied().collect();
let spins = self.loops.blocks(id).iter().any(|block| self.invented.contains(block));
if spins {
stats.missed(SPINS);
}
let writes = self
.loops
.blocks(id)
.iter()
.any(|block| func.insts(*block).any(|inst| func[inst].opcode.writes_memory()));
let mut ranges = Ranges::new(func, self.cfg, self.dom);
let mut plan = Vec::new();
let mut moved: HashSet<Value> = HashSet::new();
let mut room = [heuristics::ASSUMED_ALLOCATABLE_REGS; Class::COUNT];
for block in self.cfg.reverse_postorder() {
if !inside.contains(&block) {
continue;
}
let runs = !spins && self.post.post_dominates(block, header);
for inst in func.insts(block) {
if func.is_terminator(inst) {
continue;
}
let Some(result) = func[inst].results().next() else {
continue;
};
let Some(class) = Class::of(func[result].ty) else {
continue;
};
if !self.unchanging(func, id, inst, &moved) {
continue;
}
if writes && func[inst].opcode.touches_memory() && func.mem_in(inst).is_none() {
stats.missed(MEMORY);
continue;
}
let cost = cost(func, inst);
match movement(speculate::why_not(func, inst, &mut ranges, preheader)) {
Move::Anywhere => (),
Move::IfItWasGoingToRun if runs => (),
Move::IfItWasGoingToRun => {
stats.missed(SPECULATIVE);
continue;
}
Move::Nowhere => {
stats.missed(EFFECTS);
continue;
}
}
let bank = match class {
Class::Integer => 0,
Class::Float => 1,
};
if cost > 0 {
let tight = pressure.is_tight(self.loops, id, class, room[bank]);
if tight && cost < heuristics::LICM_EXPENSIVE {
stats.missed(PRESSURE);
continue;
}
if !fuel.take() {
stats.missed(NO_FUEL);
return trim(func, plan);
}
room[bank] = room[bank].saturating_sub(1);
}
moved.extend(func[inst].results());
plan.push(inst);
}
}
trim(func, plan)
}
fn unchanging(&self, func: &Func, id: LoopId, inst: Inst, moved: &HashSet<Value>) -> bool {
func[func[inst].args]
.iter()
.all(|arg| self.loops.is_invariant(func, id, *arg) || moved.contains(arg))
}
}
fn trim(func: &Func, plan: Vec<Inst>) -> Vec<Inst> {
let mut wanted: HashSet<Value> = HashSet::new();
let mut keep = Vec::with_capacity(plan.len());
for inst in plan.into_iter().rev() {
if cost(func, inst) == 0 && !func[inst].results().any(|value| wanted.contains(&value)) {
continue;
}
wanted.extend(func[func[inst].args].iter().copied());
keep.push(inst);
}
keep.reverse();
keep
}
fn movement(why: Option<&'static str>) -> Move {
match why {
None => Move::Anywhere,
Some(speculate::BY_ZERO | speculate::OVERFLOW | speculate::ADDRESS) => {
Move::IfItWasGoingToRun
}
Some(_) => Move::Nowhere,
}
}
fn cost(func: &Func, inst: Inst) -> u32 {
match func[inst].opcode {
Opcode::IConst | Opcode::FConst | Opcode::GlobalAddr | Opcode::BlockAddr => 0,
Opcode::Load
| Opcode::Select
| Opcode::Call
| Opcode::CallIndirect
| Opcode::Mul
| Opcode::SDiv
| Opcode::UDiv
| Opcode::SRem
| Opcode::URem
| Opcode::FMul
| Opcode::FDiv
| Opcode::FRem
| Opcode::Shl
| Opcode::LShr
| Opcode::AShr
| Opcode::ICmp
| Opcode::FCmp => heuristics::LICM_EXPENSIVE,
_ => 1,
}
}
#[cfg(test)]
mod tests {
use rucc_base::{Interner, Symbol};
use rucc_ir::{
Block, Builder, Def, Extra, Flags, Func, Global, Inst, InstData, IntPred, MemInfo,
MemOrder, Module, Opcode, Restrict, Signature, Type, Value, verify_func,
};
use rucc_target::{TargetInfo, Triple};
use super::{
EFFECTS, HOISTED, LICM, MEMORY, NO_FUEL, NO_PREHEADER, PRESSURE, SPECULATIVE, SPINS,
};
use crate::canon::Canon;
use crate::header_copy::SPEED;
use crate::stats::Kind;
use crate::{Analyses, Fuel, Pass, Stats};
fn hoist(func: &mut Func, fuel: &mut Fuel) -> Stats {
LICM.run(func, &mut Analyses::new(), fuel)
}
fn sound(func: &Func, names: &mut Interner) {
checked(func, names, &[]);
}
fn checked(func: &Func, names: &mut Interner, globals: &[Symbol]) {
let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
let mut module = Module::new(names.intern("t.c"), &target);
for name in globals {
module.add_global(Global::new(*name, 16, 8));
}
if let Err(errors) = verify_func(&module, func, names) {
panic!("{errors:#?}");
}
}
fn made(func: &Func, value: Value) -> Inst {
match func[value].def {
Def::Result { inst, .. } => inst,
other => panic!("{other:?} is not something an instruction worked out"),
}
}
fn lives_in(func: &Func, value: Value) -> Block {
func.block_of(made(func, value)).expect("it is in a block")
}
fn position(func: &Func, value: Value) -> usize {
let inst = made(func, value);
let block = func.block_of(inst).expect("it is in a block");
func.insts(block).position(|other| other == inst).expect("it is in that block")
}
fn tucked(func: &mut Func, block: Block) {
let term = func
.insts(block)
.find(|inst| func.is_terminator(*inst))
.expect("the block ends in something");
let stragglers: Vec<Inst> =
func.insts(block).skip_while(|inst| *inst != term).skip(1).collect();
for inst in stragglers {
func.remove_inst(inst);
func.insert_before(inst, term);
}
}
fn record(size: u64) -> MemInfo {
MemInfo { size, align: 8, order: MemOrder::NotAtomic, tbaa: None, restrict: Restrict::NONE }
}
struct Counted {
names: Interner,
func: Func,
entry: Block,
head: Block,
body: Block,
limit: Value,
pointer: Value,
}
fn counted(spare: usize) -> Counted {
let mut names = Interner::new();
let mut types = vec![Type::int(32); spare + 1];
types.push(Type::PTR);
let signature = Signature::new().with_params(&types).with_returns(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let body = func.create_block();
let done = func.create_block();
let handed: Vec<Value> =
types.iter().map(|ty| func.append_param(entry, *ty)).collect::<Vec<_>>();
let limit = handed[0];
let pointer = *handed.last().expect("the pointer is the last of them");
let i = func.append_param(head, Type::int(32));
let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
let one = Builder::new(&mut func, body).iconst(Type::int(32), 1);
let next = Builder::new(&mut func, body).binary(Opcode::Add, i, one, Flags::NONE);
Builder::new(&mut func, body).jump(head, &[next]);
let mut build = Builder::new(&mut func, done);
let mut total = i;
for value in &handed[1..=spare] {
total = build.binary(Opcode::Add, total, *value, Flags::NONE);
}
build.ret(&[total]);
Counted { names, func, entry, head, body, limit, pointer }
}
#[test]
fn an_invariant_computation_moves_in_front_of_the_loop() {
let mut it = counted(0);
let product = Builder::new(&mut it.func, it.body).binary(
Opcode::Mul,
it.limit,
it.limit,
Flags::NONE,
);
tucked(&mut it.func, it.body);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
assert_eq!(lives_in(&it.func, product), it.entry, "it is in front of the loop now");
sound(&it.func, &mut it.names);
}
#[test]
fn a_computation_the_loop_changes_stays_where_it_is() {
let mut it = counted(0);
let i = it.func[it.head].params[0];
let square = Builder::new(&mut it.func, it.body).binary(Opcode::Mul, i, i, Flags::NONE);
tucked(&mut it.func, it.body);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 0);
assert_eq!(lives_in(&it.func, square), it.body);
sound(&it.func, &mut it.names);
}
#[test]
fn a_loop_with_nothing_invariant_in_it_is_left_alone() {
let mut it = counted(0);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 0);
sound(&it.func, &mut it.names);
}
#[test]
fn a_load_the_loop_might_not_reach_stays_where_it_is() {
let mut it = counted(0);
let read = Builder::new(&mut it.func, it.body).load(
Type::int(32),
it.pointer,
record(4),
Flags::NONE,
);
tucked(&mut it.func, it.body);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 0);
assert_eq!(stats.count(Kind::Missed, SPECULATIVE), 1);
assert_eq!(lives_in(&it.func, read), it.body, "the loop may run zero times");
sound(&it.func, &mut it.names);
}
#[test]
fn the_same_load_moves_once_the_loop_tests_at_the_bottom() {
let mut it = counted(0);
let read = Builder::new(&mut it.func, it.body).load(
Type::int(32),
it.pointer,
record(4),
Flags::NONE,
);
tucked(&mut it.func, it.body);
let mut an = Analyses::new();
Canon.run(&mut it.func, &mut an, &mut Fuel::unlimited());
SPEED.run(&mut it.func, &mut an, &mut Fuel::unlimited());
Canon.run(&mut it.func, &mut an, &mut Fuel::unlimited());
let stats = LICM.run(&mut it.func, &mut an, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
assert_ne!(lives_in(&it.func, read), it.body, "it left the body");
sound(&it.func, &mut it.names);
}
#[test]
fn something_that_could_trap_moves_when_it_runs_on_every_entry() {
let mut it = counted(0);
let share = Builder::new(&mut it.func, it.head).binary(
Opcode::SDiv,
it.limit,
it.limit,
Flags::NONE,
);
tucked(&mut it.func, it.head);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
assert_eq!(lives_in(&it.func, share), it.entry);
sound(&it.func, &mut it.names);
}
#[test]
fn a_division_a_test_inside_the_loop_made_safe_stays_inside_that_test() {
let mut names = Interner::new();
let signature =
Signature::new().with_params(&[Type::int(32)]).with_returns(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let body = func.create_block();
let safe = func.create_block();
let latch = func.create_block();
let done = func.create_block();
let limit = func.append_param(entry, Type::int(32));
let i = func.append_param(head, Type::int(32));
let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
let guard = Builder::new(&mut func, body).icmp(IntPred::Ne, limit, zero);
Builder::new(&mut func, body).br_if(guard, safe, &[], latch, &[]);
let share = Builder::new(&mut func, safe).binary(Opcode::SDiv, limit, limit, Flags::NONE);
Builder::new(&mut func, safe).jump(latch, &[]);
let one = Builder::new(&mut func, latch).iconst(Type::int(32), 1);
let next = Builder::new(&mut func, latch).binary(Opcode::Add, i, one, Flags::NONE);
Builder::new(&mut func, latch).jump(head, &[next]);
Builder::new(&mut func, done).ret(&[i]);
let stats = hoist(&mut func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Missed, SPECULATIVE), 1);
assert_eq!(lives_in(&func, share), safe, "the guard is what made it safe");
assert_eq!(lives_in(&func, guard), entry);
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
sound(&func, &mut names);
}
#[test]
fn the_same_division_in_the_body_stays() {
let mut it = counted(0);
let share = Builder::new(&mut it.func, it.body).binary(
Opcode::SDiv,
it.limit,
it.limit,
Flags::NONE,
);
tucked(&mut it.func, it.body);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Missed, SPECULATIVE), 1);
assert_eq!(lives_in(&it.func, share), it.body, "the divisor could be zero");
sound(&it.func, &mut it.names);
}
#[test]
fn a_volatile_load_stays_even_where_it_runs_on_every_entry() {
let mut it = counted(0);
let read = Builder::new(&mut it.func, it.head).load(
Type::int(32),
it.pointer,
record(4),
Flags::VOLATILE,
);
tucked(&mut it.func, it.head);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Missed, EFFECTS), 1);
assert_eq!(lives_in(&it.func, read), it.head, "one access per iteration is the point");
sound(&it.func, &mut it.names);
}
#[test]
fn a_chain_comes_out_in_the_order_it_was_written_in() {
let mut it = counted(0);
let mut build = Builder::new(&mut it.func, it.body);
let product = build.binary(Opcode::Mul, it.limit, it.limit, Flags::NONE);
let sum = build.binary(Opcode::Mul, product, it.limit, Flags::NONE);
tucked(&mut it.func, it.body);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 2);
assert_eq!(lives_in(&it.func, product), it.entry);
assert_eq!(lives_in(&it.func, sum), it.entry);
assert!(position(&it.func, product) < position(&it.func, sum));
sound(&it.func, &mut it.names);
}
#[test]
fn the_pass_stops_where_the_fuel_runs_out() {
let mut it = counted(0);
let mut build = Builder::new(&mut it.func, it.body);
let product = build.binary(Opcode::Mul, it.limit, it.limit, Flags::NONE);
build.binary(Opcode::Mul, product, it.limit, Flags::NONE);
tucked(&mut it.func, it.body);
let stats = hoist(&mut it.func, &mut Fuel::of(1));
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
assert_eq!(stats.count(Kind::Missed, NO_FUEL), 1);
sound(&it.func, &mut it.names);
}
#[test]
fn a_cheap_computation_stays_where_the_loop_is_already_full() {
let mut it = counted(14);
let mut build = Builder::new(&mut it.func, it.body);
let sum = build.binary(Opcode::Add, it.limit, it.limit, Flags::NONE);
let product = build.binary(Opcode::Mul, it.limit, it.limit, Flags::NONE);
tucked(&mut it.func, it.body);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Missed, PRESSURE), 1);
assert_eq!(lives_in(&it.func, sum), it.body);
assert_eq!(lives_in(&it.func, product), it.entry);
sound(&it.func, &mut it.names);
}
#[test]
fn the_same_add_moves_when_the_loop_has_room() {
let mut it = counted(0);
let sum = Builder::new(&mut it.func, it.body).binary(
Opcode::Add,
it.limit,
it.limit,
Flags::NONE,
);
tucked(&mut it.func, it.body);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Missed, PRESSURE), 0);
assert_eq!(lives_in(&it.func, sum), it.entry);
sound(&it.func, &mut it.names);
}
#[test]
fn a_loop_with_two_ways_in_is_left_alone() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::I1, Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let low = func.create_block();
let high = func.create_block();
let head = func.create_block();
let body = func.create_block();
let done = func.create_block();
let either = func.append_param(entry, Type::I1);
let n = func.append_param(entry, Type::int(32));
let i = func.append_param(head, Type::int(32));
Builder::new(&mut func, entry).br_if(either, low, &[], high, &[]);
let zero = Builder::new(&mut func, low).iconst(Type::int(32), 0);
Builder::new(&mut func, low).jump(head, &[zero]);
let one = Builder::new(&mut func, high).iconst(Type::int(32), 1);
Builder::new(&mut func, high).jump(head, &[one]);
let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, n);
Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
let product = Builder::new(&mut func, body).binary(Opcode::Mul, n, n, Flags::NONE);
let next = Builder::new(&mut func, body).binary(Opcode::Add, i, product, Flags::NONE);
Builder::new(&mut func, body).jump(head, &[next]);
Builder::new(&mut func, done).ret(&[]);
let stats = hoist(&mut func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Missed, NO_PREHEADER), 1);
assert_eq!(lives_in(&func, product), body);
sound(&func, &mut names);
}
#[test]
fn a_loop_with_no_way_out_gets_the_pure_hoist_and_not_the_other_one() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32), Type::PTR]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let n = func.append_param(entry, Type::int(32));
let pointer = func.append_param(entry, Type::PTR);
Builder::new(&mut func, entry).jump(head, &[]);
let mut build = Builder::new(&mut func, head);
let product = build.binary(Opcode::Mul, n, n, Flags::NONE);
let read = build.load(Type::int(32), pointer, record(4), Flags::NONE);
build.jump(head, &[]);
let stats = hoist(&mut func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Missed, SPINS), 1);
assert_eq!(stats.count(Kind::Missed, SPECULATIVE), 1);
assert_eq!(lives_in(&func, product), entry, "arithmetic is safe anywhere");
assert_eq!(lives_in(&func, read), head, "the address is still one nobody has vouched for");
sound(&func, &mut names);
}
#[test]
fn an_invariant_comes_all_the_way_out_of_a_nest_in_one_run() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let outer = func.create_block();
let ready = func.create_block();
let inner = func.create_block();
let deep = func.create_block();
let latch = func.create_block();
let done = func.create_block();
let n = func.append_param(entry, Type::int(32));
let i = func.append_param(outer, Type::int(32));
let j = func.append_param(inner, Type::int(32));
let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
Builder::new(&mut func, entry).jump(outer, &[zero]);
let outer_test = Builder::new(&mut func, outer).icmp(IntPred::Slt, i, n);
Builder::new(&mut func, outer).br_if(outer_test, ready, &[], done, &[]);
let start = Builder::new(&mut func, ready).iconst(Type::int(32), 0);
Builder::new(&mut func, ready).jump(inner, &[start]);
let inner_test = Builder::new(&mut func, inner).icmp(IntPred::Slt, j, n);
Builder::new(&mut func, inner).br_if(inner_test, deep, &[], latch, &[]);
let mut build = Builder::new(&mut func, deep);
let product = build.binary(Opcode::Mul, n, n, Flags::NONE);
let one = build.iconst(Type::int(32), 1);
let next_j = build.binary(Opcode::Add, j, one, Flags::NONE);
build.jump(inner, &[next_j]);
let mut build = Builder::new(&mut func, latch);
let step = build.iconst(Type::int(32), 1);
let next_i = build.binary(Opcode::Add, i, step, Flags::NONE);
build.jump(outer, &[next_i]);
Builder::new(&mut func, done).ret(&[]);
let stats = hoist(&mut func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 2, "one level and then the other");
assert_eq!(lives_in(&func, product), entry);
sound(&func, &mut names);
}
#[test]
fn a_function_with_no_loop_in_it_is_untouched() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
Builder::new(&mut func, entry).ret(&[]);
let stats = hoist(&mut func, &mut Fuel::unlimited());
assert!(!stats.changed());
sound(&func, &mut names);
}
#[test]
fn the_address_of_a_global_moves_only_when_something_that_reads_it_moves() {
let mut it = counted(0);
let grid = it.names.intern("grid");
let mut build = Builder::new(&mut it.func, it.head);
let at = build.value(
InstData { extra: Extra::Symbol(grid), ..InstData::new(Opcode::GlobalAddr) },
Type::PTR,
);
let read = build.load(Type::int(32), at, record(4), Flags::NONE);
tucked(&mut it.func, it.head);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 2, "the load and its address");
assert_eq!(lives_in(&it.func, at), it.entry);
assert_eq!(lives_in(&it.func, read), it.entry);
assert!(position(&it.func, at) < position(&it.func, read));
checked(&it.func, &mut it.names, &[grid]);
}
#[test]
fn the_address_of_a_global_on_its_own_stays_where_it_is() {
let mut it = counted(0);
let grid = it.names.intern("grid");
let at = Builder::new(&mut it.func, it.body).value(
InstData { extra: Extra::Symbol(grid), ..InstData::new(Opcode::GlobalAddr) },
Type::PTR,
);
tucked(&mut it.func, it.body);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 0);
assert_eq!(lives_in(&it.func, at), it.body);
checked(&it.func, &mut it.names, &[grid]);
}
#[test]
fn the_same_load_stays_once_the_loop_writes_anything_at_all() {
let mut it = counted(0);
let mem = it.func.add_mem(record(4));
let slot = Builder::new(&mut it.func, it.entry)
.value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR);
tucked(&mut it.func, it.entry);
let mut build = Builder::new(&mut it.func, it.body);
let read = build.load(Type::int(32), slot, record(4), Flags::NONE);
build.store(read, it.pointer, record(4), Flags::NONE);
tucked(&mut it.func, it.body);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Missed, MEMORY), 1);
assert_eq!(lives_in(&it.func, read), it.body);
sound(&it.func, &mut it.names);
}
#[test]
fn a_load_of_a_local_the_loop_does_not_write_moves_out_of_the_body() {
let mut it = counted(0);
let mem = it.func.add_mem(record(4));
let slot = Builder::new(&mut it.func, it.entry)
.value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR);
tucked(&mut it.func, it.entry);
let read =
Builder::new(&mut it.func, it.body).load(Type::int(32), slot, record(4), Flags::NONE);
tucked(&mut it.func, it.body);
let stats = hoist(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
assert_eq!(lives_in(&it.func, read), it.entry, "four bytes of four are always there");
sound(&it.func, &mut it.names);
}
}