use std::collections::HashMap;
use rucc_ir::{Block, Flags, Func, Inst, Opcode, Type, Value};
use crate::uses::substitute;
use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
const FORWARDED: &str = "load replaced by the value a store in the same block wrote there";
const REUSED: &str = "load replaced by what an earlier load of the same address read";
const WIDTH: &str = "load kept, what is known about that address is a different type";
const NO_FUEL: &str = "redundant load kept, the pass ran out of fuel";
#[derive(Debug)]
pub struct LoadForward;
impl Pass for LoadForward {
fn name(&self) -> &'static str {
"load-forward"
}
fn describe(&self) -> &'static str {
"a load of an address the block has already read or written is that value"
}
fn preserves(&self) -> Preserved {
Preserved::ALL.without(Analysis::Liveness)
}
fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
let mut stats = Stats::new();
let mut forward: HashMap<Value, Value> = HashMap::new();
let mut gone: Vec<Inst> = Vec::new();
for block in func.blocks().collect::<Vec<Block>>() {
let mut known: HashMap<Value, Held> = HashMap::new();
for inst in func.insts(block).collect::<Vec<Inst>>() {
match act(func, inst) {
Act::Ignore => {}
Act::Forget => known.clear(),
Act::Wrote { address, value, ty } => {
known.clear();
known.insert(address, Held { ty, value, stored: true });
}
Act::Read { address, result, ty } => {
match known.get(&address).copied() {
Some(held) if held.ty == ty => {
if fuel.take() {
forward.insert(result, held.value);
gone.push(inst);
stats.optimized(if held.stored { FORWARDED } else { REUSED });
continue;
}
stats.missed(NO_FUEL);
}
Some(_) => stats.missed(WIDTH),
None => {}
}
known.insert(address, Held { ty, value: result, stored: false });
}
}
}
}
for inst in gone {
func.remove_inst(inst);
}
if !forward.is_empty() {
substitute(func, &forward);
}
stats
}
}
#[derive(Clone, Copy)]
struct Held {
ty: Type,
value: Value,
stored: bool,
}
enum Act {
Ignore,
Forget,
Wrote { address: Value, value: Value, ty: Type },
Read { address: Value, result: Value, ty: Type },
}
fn act(func: &Func, inst: Inst) -> Act {
let data = &func[inst];
if !data.opcode.touches_memory() {
return Act::Ignore;
}
if data.flags.contains(Flags::VOLATILE) {
return Act::Forget;
}
let args = &func[data.args];
match data.opcode {
Opcode::Load => {
let mut results = data.results();
let (Some(&address), Some(result), None) =
(args.first(), results.next(), results.next())
else {
return Act::Forget;
};
Act::Read { address, result, ty: func[result].ty }
}
Opcode::Store => {
let (Some(&value), Some(&address)) = (args.first(), args.get(1)) else {
return Act::Forget;
};
Act::Wrote { address, value, ty: func[value].ty }
}
_ => Act::Forget,
}
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Block, Builder, Extra, Flags, Func, InstData, MemInfo, MemOrder, Restrict, Signature, Type,
Value,
};
use super::*;
use crate::Fuel;
fn blank() -> (Interner, Func, Block) {
let mut names = Interner::new();
let name = names.intern("f");
let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(64)]));
let block = func.create_block();
(names, func, block)
}
fn plain(align: u32) -> MemInfo {
MemInfo { size: 0, align, order: MemOrder::NotAtomic, tbaa: None, restrict: Restrict::NONE }
}
fn local(build: &mut Builder<'_>) -> Value {
let mem = build.func().add_mem(MemInfo { size: 8, ..plain(8) });
build.value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR)
}
fn run(func: &mut Func) -> Stats {
LoadForward.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
}
fn loads(func: &Func) -> usize {
func.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
.filter(|&inst| func[inst].opcode == Opcode::Load)
.count()
}
fn returned(func: &Func) -> Vec<Value> {
let block = func.blocks().next().expect("the function has a block");
let inst = func.terminator(block).expect("the block has a terminator");
func[func[inst].args].to_vec()
}
#[test]
fn a_load_of_what_a_store_just_wrote_is_the_stored_value() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let slot = local(&mut build);
let wrote = build.iconst(Type::int(64), 7);
build.store(wrote, slot, plain(8), Flags::NONE);
let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.ret(&[read]);
let stats = run(&mut func);
assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
assert_eq!(loads(&func), 0, "the load itself has to go, nothing else would remove it");
assert_eq!(returned(&func), vec![wrote]);
}
#[test]
fn the_second_load_of_an_address_is_what_the_first_one_read() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let slot = local(&mut build);
let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
let sum = build.binary(Opcode::Add, first, second, Flags::NONE);
build.ret(&[sum]);
let stats = run(&mut func);
assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
assert_eq!(loads(&func), 1, "one read of that address has to happen and only one");
let sum = returned(&func)[0];
let rucc_ir::Def::Result { inst, .. } = func[sum].def else { panic!("the add is gone") };
assert_eq!(func[func[inst].args].to_vec(), vec![first, first]);
}
#[test]
fn a_store_of_a_different_width_is_not_forwarded_through() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let slot = local(&mut build);
let wrote = build.iconst(Type::int(32), 7);
build.store(wrote, slot, plain(4), Flags::NONE);
let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.ret(&[read]);
let stats = run(&mut func);
assert_eq!(stats.count(crate::stats::Kind::Missed, WIDTH), 1);
assert_eq!(loads(&func), 1, "a four byte store does not say what eight bytes hold");
assert_eq!(returned(&func), vec![read]);
}
#[test]
fn a_call_between_the_two_accesses_is_a_write_to_everything() {
let (mut names, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let slot = local(&mut build);
let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
let signature = build.func().add_signature(Signature::new());
build.call(names.intern("g"), signature, &[]);
let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.ret(&[first, second]);
let stats = run(&mut func);
assert!(!stats.changed(), "nothing here says what the call did to that address");
assert_eq!(loads(&func), 2);
}
#[test]
fn a_store_to_another_address_is_a_write_to_everything_too() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let slot = local(&mut build);
let other = local(&mut build);
let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.store(first, other, plain(8), Flags::NONE);
let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.ret(&[first, second]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(loads(&func), 2);
}
#[test]
fn a_volatile_load_is_not_reused_and_nothing_before_it_survives_it() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let slot = local(&mut build);
let first = build.load(Type::int(64), slot, plain(8), Flags::VOLATILE);
let second = build.load(Type::int(64), slot, plain(8), Flags::VOLATILE);
let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.ret(&[first, second, third]);
let stats = run(&mut func);
assert!(!stats.changed(), "every volatile read has to happen");
assert_eq!(loads(&func), 3);
}
#[test]
fn what_one_block_knows_does_not_reach_the_next_one() {
let (_, mut func, entry) = blank();
let next = func.create_block();
let mut build = Builder::new(&mut func, entry);
let slot = local(&mut build);
let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.jump(next, &[]);
let mut build = Builder::new(&mut func, next);
let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.ret(&[first, second]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(loads(&func), 2);
}
#[test]
fn a_chain_of_reads_all_come_from_the_first_one() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let slot = local(&mut build);
let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.load(Type::int(64), slot, plain(8), Flags::NONE);
let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.ret(&[third]);
let stats = run(&mut func);
assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 2);
assert_eq!(loads(&func), 1);
assert_eq!(returned(&func), vec![first]);
}
#[test]
fn a_store_of_a_value_the_pass_is_removing_forwards_to_where_that_value_went() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let slot = local(&mut build);
let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.store(second, slot, plain(8), Flags::NONE);
let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.ret(&[third]);
let stats = run(&mut func);
assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
assert_eq!(loads(&func), 1);
assert_eq!(returned(&func), vec![first]);
}
#[test]
fn without_fuel_the_load_stays_and_the_chance_is_still_counted() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let slot = local(&mut build);
let wrote = build.iconst(Type::int(64), 7);
build.store(wrote, slot, plain(8), Flags::NONE);
let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
build.ret(&[read]);
let stats =
LoadForward.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::of(0));
assert!(!stats.changed());
assert_eq!(stats.count(crate::stats::Kind::Missed, NO_FUEL), 1);
assert_eq!(loads(&func), 1);
}
}