use std::collections::HashMap;
use rucc_ir::{Block, Flags, Func, Inst, Opcode, Type, Value};
use crate::memssa::{Clobber, Walk};
use crate::uses::substitute;
use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats, memssa};
pub const NAME: &str = "redundant-load";
const FORWARDED: &str = "load replaced by the value of the store the walk found";
const PARTIAL: &str = "load kept, what wrote it covers only part of what it reads";
const MAYBE: &str = "load kept, something that may have written it could not be pinned down";
const UNKNOWN: &str = "load kept, the walk back over memory established nothing";
const WIDTH: &str = "load kept, the store that covers it wrote a different type";
const NOT_A_STORE: &str = "load kept, what covers it writes memory without storing one value";
const NO_FUEL: &str = "redundant load kept, the pass ran out of fuel";
#[derive(Debug)]
pub struct RedundantLoad;
impl Pass for RedundantLoad {
fn name(&self) -> &'static str {
NAME
}
fn describe(&self) -> &'static str {
"a load takes the value of the store it sees, wherever in the function that store is"
}
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();
if !memssa::build(func) {
return stats;
}
let mut forward: HashMap<Value, Value> = HashMap::new();
let mut gone: Vec<Inst> = Vec::new();
{
let mut walk = Walk::new(func, an.outside());
for block in func.blocks().collect::<Vec<Block>>() {
for inst in func.insts(block).collect::<Vec<Inst>>() {
let Some((result, ty)) = reads(func, inst) else {
continue;
};
match walk.clobber(inst) {
Clobber::Exact(wrote) => {
let Some(value) = stored(func, wrote) else {
stats.missed(NOT_A_STORE);
continue;
};
if func[value].ty != ty {
stats.missed(WIDTH);
continue;
}
if !fuel.take() {
stats.missed(NO_FUEL);
continue;
}
forward.insert(result, value);
gone.push(inst);
stats.optimized(FORWARDED);
}
Clobber::Partial(_) => stats.missed(PARTIAL),
Clobber::Maybe(_) => stats.missed(MAYBE),
Clobber::Unknown => stats.missed(UNKNOWN),
Clobber::NoClobber => {}
}
}
}
let counts = walk.counts();
if counts.walks() > 0 {
stats.record(crate::stats::Kind::Note, WALKS, count(counts.walks()));
stats.record(crate::stats::Kind::Note, STEPS, count(counts.steps()));
if counts.exhausted() > 0 {
stats.record(crate::stats::Kind::Note, EXHAUSTED, count(counts.exhausted()));
}
}
}
for inst in gone {
func.remove_inst(inst);
}
if !forward.is_empty() {
substitute(func, &forward);
}
memssa::strip(func);
an.settle(func, self.preserves(), false);
stats
}
}
const WALKS: &str = "walks back over memory";
const STEPS: &str = "memory defs the walks looked at";
const EXHAUSTED: &str = "walks that ran out of budget";
fn count(of: u64) -> u32 {
u32::try_from(of).unwrap_or(u32::MAX)
}
fn reads(func: &Func, inst: Inst) -> Option<(Value, Type)> {
let data = &func[inst];
if data.opcode != Opcode::Load || data.flags.contains(Flags::VOLATILE) {
return None;
}
let mut results = data.results();
let (Some(result), None) = (results.next(), results.next()) else {
return None;
};
Some((result, func[result].ty))
}
fn stored(func: &Func, inst: Inst) -> Option<Value> {
let data = &func[inst];
if data.opcode != Opcode::Store || data.flags.contains(Flags::VOLATILE) {
return None;
}
func[data.args].first().copied()
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use rucc_base::Interner;
use rucc_ir::{Module, parse, verify_func};
use super::*;
use crate::outside::Outside;
const HEADER: &str = "\
; ModuleID = 'mem.c'
; format 0
target triple = \"x86_64-unknown-linux-gnu\"
target datalayout = \"e-p:64:64-i64:64-f80:128-S128\"
";
fn wrap(signature: &str, body: &str) -> String {
format!("{HEADER}\nfunc @f{signature}, linkage(external) {{\n{body}}}\n")
}
fn run(text: &str) -> (Module, Stats) {
let mut names = Interner::new();
let mut module = parse(text, &mut names).expect("the text parses");
let id = module.funcs().next().expect("one function");
let outside = Arc::new(Outside::of(&module));
let mut an = crate::machine::fixtures::analyses().about(outside);
let stats = RedundantLoad.run(&mut module[id], &mut an, &mut Fuel::unlimited());
if let Err(errors) = verify_func(&module, &module[id], &names) {
panic!("{errors:#?}");
}
(module, stats)
}
fn one(module: &Module) -> &Func {
&module[module.funcs().next().expect("one function")]
}
fn count_of(func: &Func, opcode: Opcode) -> usize {
func.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
.filter(|&inst| func[inst].opcode == opcode)
.count()
}
fn off(func: &Func) {
for block in func.blocks() {
assert!(func[block].params.iter().all(|¶m| !func[param].ty.is_mem()));
for inst in func.insts(block) {
assert_ne!(func[inst].opcode, Opcode::MemEntry);
assert!(!func.carries_mem(inst));
}
}
}
#[test]
fn a_store_in_a_block_above_the_load_reaches_it() {
let text = wrap(
"(ptr, i1) -> i32",
"block0(%0: ptr, %1: i1):
%2 = iconst.i32 7
store %2 -> %0, align 4
br_if %1, block1, block2
block1:
jump block2
block2:
%3 = load.i32 %0, align 4
return %3
",
);
let (module, stats) = run(&text);
assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
let func = one(&module);
off(func);
assert_eq!(count_of(func, Opcode::Load), 0, "the load is still there");
let ret = func
.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
.find(|&inst| func[inst].opcode == Opcode::Return)
.expect("a return");
let returned = func[func[ret].args][0];
let seven = func
.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
.find(|&inst| func[inst].opcode == Opcode::IConst)
.expect("the constant");
assert_eq!(returned, func[seven].results().next().expect("a result"));
}
#[test]
fn a_store_down_only_one_arm_is_not_an_answer() {
let text = wrap(
"(ptr, i1) -> i32",
"block0(%0: ptr, %1: i1):
br_if %1, block1, block2
block1:
%2 = iconst.i32 7
store %2 -> %0, align 4
jump block3
block2:
jump block3
block3:
%3 = load.i32 %0, align 4
return %3
",
);
let (module, stats) = run(&text);
assert!(!stats.changed(), "a store on one path is not the value on both");
assert_eq!(stats.count(crate::stats::Kind::Missed, UNKNOWN), 1);
off(one(&module));
}
#[test]
fn both_arms_storing_the_same_way_is_still_two_stores() {
let text = wrap(
"(ptr, i1) -> i32",
"block0(%0: ptr, %1: i1):
%2 = iconst.i32 7
br_if %1, block1, block2
block1:
store %2 -> %0, align 4
jump block3
block2:
store %2 -> %0, align 4
jump block3
block3:
%3 = load.i32 %0, align 4
return %3
",
);
let (_, stats) = run(&text);
assert!(!stats.changed());
}
#[test]
fn a_loop_that_writes_nothing_keeps_the_store_above_it() {
let text = wrap(
"(ptr, i1) -> i32",
"block0(%0: ptr, %1: i1):
%2 = iconst.i32 7
store %2 -> %0, align 4
jump block1
block1:
%3 = load.i32 %0, align 4
br_if %1, block1, block2
block2:
return %3
",
);
let (module, stats) = run(&text);
assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
assert_eq!(count_of(one(&module), Opcode::Load), 0);
}
#[test]
fn a_store_inside_the_loop_stops_it() {
let text = wrap(
"(ptr, i1) -> i32",
"block0(%0: ptr, %1: i1):
%2 = iconst.i32 7
store %2 -> %0, align 4
jump block1
block1:
%3 = load.i32 %0, align 4
%4 = add %3, %3
store %4 -> %0, align 4
br_if %1, block1, block2
block2:
return %3
",
);
let (_, stats) = run(&text);
assert!(!stats.changed(), "the body writes what the load reads");
}
#[test]
fn the_bytes_being_the_same_is_not_the_type_being_the_same() {
let text = wrap(
"(ptr) -> f32",
"block0(%0: ptr):
%1 = iconst.i32 7
store %1 -> %0, align 4
%2 = load.f32 %0, align 4
return %2
",
);
let (_, stats) = run(&text);
assert!(!stats.changed());
assert_eq!(stats.count(crate::stats::Kind::Missed, WIDTH), 1);
}
#[test]
fn a_volatile_load_has_to_happen() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = iconst.i32 7
store %1 -> %0, align 4
%2 = load.i32.volatile %0, align 4
return %2
",
);
let (module, stats) = run(&text);
assert!(!stats.changed());
assert_eq!(count_of(one(&module), Opcode::Load), 1);
}
#[test]
fn a_function_with_no_memory_in_it_is_left_alone() {
let text = wrap(
"(i32) -> i32",
"block0(%0: i32):
%1 = add %0, %0
return %1
",
);
let (module, stats) = run(&text);
assert!(stats.is_empty(), "there was nothing here to say anything about");
off(one(&module));
}
#[test]
fn out_of_fuel_keeps_the_load_and_still_counts_it() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = iconst.i32 7
store %1 -> %0, align 4
%2 = load.i32 %0, align 4
return %2
",
);
let mut names = Interner::new();
let mut module = parse(&text, &mut names).expect("the text parses");
let id = module.funcs().next().expect("one function");
let outside = Arc::new(Outside::of(&module));
let mut an = crate::machine::fixtures::analyses().about(outside);
let stats = RedundantLoad.run(&mut module[id], &mut an, &mut Fuel::of(0));
assert!(!stats.changed());
assert_eq!(stats.count(crate::stats::Kind::Missed, NO_FUEL), 1);
off(&module[id]);
}
#[test]
fn what_the_walks_cost_is_written_down() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = iconst.i32 7
store %1 -> %0, align 4
%2 = load.i32 %0, align 4
return %2
",
);
let (_, stats) = run(&text);
assert_eq!(stats.count(crate::stats::Kind::Note, WALKS), 1);
assert_eq!(stats.count(crate::stats::Kind::Note, STEPS), 1);
assert_eq!(stats.count(crate::stats::Kind::Note, EXHAUSTED), 0);
}
}