use std::collections::HashMap;
use rucc_ir::{Block, Extra, Flags, Func, Inst, Opcode, Restrict, Type, Value};
use crate::alias::{Access, origin};
use crate::memssa::{Clobber, Step, 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 REUSED: &str = "load replaced by the value an earlier load of the same address already had";
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 body: &Func = func;
let dom = an.dominators(body);
let mut walk = Walk::new(body, an.outside()).knowing(an.modref());
let mut seen: HashMap<(Value, Value, Type), (Block, Value)> = HashMap::new();
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;
};
let key = func.mem_in(inst).map(|mem| (mem, func[func[inst].args][0], ty));
let found = match walk.clobber_with(inst, &mut |reference, def| {
through(body, reference, def).map_or(Step::Stop, Step::Retry)
}) {
Clobber::Exact(wrote) => match stored(func, wrote) {
None => Found::Kept(Some(NOT_A_STORE)),
Some(value) if func[value].ty != ty => Found::Kept(Some(WIDTH)),
Some(value) => Found::Store(value),
},
Clobber::Partial(_) => Found::Kept(Some(PARTIAL)),
Clobber::Maybe(_) => Found::Kept(Some(MAYBE)),
Clobber::Unknown => Found::Kept(Some(UNKNOWN)),
Clobber::NoClobber => Found::Kept(None),
};
let found = match found {
Found::Kept(reason) => match key.and_then(|key| seen.get(&key)) {
Some(&(at, value)) if dom.dominates(at, block) => Found::Earlier(value),
_ => Found::Kept(reason),
},
taken => taken,
};
let (value, why) = match found {
Found::Store(value) => (value, FORWARDED),
Found::Earlier(value) => (value, REUSED),
Found::Kept(reason) => {
if let Some(reason) = reason {
stats.missed(reason);
}
remember(&mut seen, key, block, result);
continue;
}
};
if !fuel.take() {
stats.missed(NO_FUEL);
remember(&mut seen, key, block, result);
continue;
}
forward.insert(result, value);
gone.push(inst);
stats.optimized(why);
remember(&mut seen, key, block, value);
}
}
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()));
}
if counts.rewritten() > 0 {
stats.record(crate::stats::Kind::Note, REWRITTEN, count(counts.rewritten()));
}
}
}
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
}
}
fn through(func: &Func, reference: &Access, inst: Inst) -> Option<Access> {
let data = func[inst];
if !matches!(data.opcode, Opcode::Memcpy | Opcode::Memmove)
|| data.flags.contains(Flags::VOLATILE)
{
return None;
}
let Extra::Mem(info) = data.extra else {
return None;
};
let args = &func[data.args];
let (&to, &from) = (args.first()?, args.get(1)?);
let (to_origin, Some(to_offset)) = origin(func, to) else {
return None;
};
let (from_origin, Some(from_offset)) = origin(func, from) else {
return None;
};
if reference.origin != to_origin {
return None;
}
let (start, end) = reference.range()?;
let (wrote, length) = (i128::from(to_offset), i128::from(func[info].size));
if start < wrote || end > wrote + length {
return None;
}
Some(Access {
origin: from_origin,
offset: Some(i64::try_from(i128::from(from_offset) + (start - wrote)).ok()?),
size: reference.size,
tbaa: None,
restrict: Restrict::NONE,
volatile: reference.volatile,
})
}
enum Found {
Store(Value),
Earlier(Value),
Kept(Option<&'static str>),
}
fn remember(
seen: &mut HashMap<(Value, Value, Type), (Block, Value)>,
key: Option<(Value, Value, Type)>,
block: Block,
value: Value,
) {
if let Some(key) = key {
seen.entry(key).or_insert((block, value));
}
}
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";
const REWRITTEN: &str = "references rewritten to what a copy took them from";
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_swap_down_one_arm_is_not_an_answer_for_the_join_below_it() {
let text = wrap(
"(i1) -> i64",
"block0(%0: i1):
%1 = alloca, size 16, align 8
%2 = alloca, size 16, align 8
%3 = alloca, size 16, align 8
%4 = iconst.i64 11
store %4 -> %1, align 8
%5 = iconst.i64 22
store %5 -> %2, align 8
br_if %0, block1, block2
block1:
memcpy %3, %1, size 16, align 8
memcpy %1, %2, size 16, align 8
memcpy %2, %3, size 16, align 8
jump block3
block2:
jump block3
block3:
%6 = load.i64 %1, align 8
return %6
",
);
let (module, stats) = run(&text);
assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 0);
assert_eq!(count_of(one(&module), Opcode::Load), 1);
}
#[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 the_same_address_read_twice_over_is_read_once() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = load.i32 %0, align 4
%2 = load.i32 %0, align 4
%3 = add %1, %2
return %3
",
);
let (module, stats) = run(&text);
assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
let func = one(&module);
off(func);
assert_eq!(count_of(func, Opcode::Load), 1);
}
#[test]
fn a_check_between_them_is_not_a_write() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = load.i32 %0, align 4
%2 = cap_of %0
check_bounds %2, %0, size 4, align 4
%3 = load.i32 %0, align 4
%4 = add %1, %3
return %4
",
);
let (module, stats) = run(&text);
assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
let func = one(&module);
assert_eq!(count_of(func, Opcode::Load), 1);
assert_eq!(count_of(func, Opcode::CheckBounds), 1);
}
#[test]
fn a_write_that_may_be_the_same_address_ends_it() {
let text = wrap(
"(ptr, ptr) -> i32",
"block0(%0: ptr, %1: ptr):
%2 = load.i32 %0, align 4
%3 = iconst.i32 7
store %3 -> %1, align 4
%4 = load.i32 %0, align 4
%5 = add %2, %4
return %5
",
);
let (module, stats) = run(&text);
assert!(!stats.changed());
assert_eq!(count_of(one(&module), Opcode::Load), 2);
}
#[test]
fn a_setjmp_between_them_ends_it_even_for_a_local_nobody_else_can_see() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = alloca, size 4, align 4
%2 = iconst.i32 0
store %2 -> %1, align 4
%3 = setjmp_marker.i32 %0
%4 = load.i32 %1, align 4
return %4
",
);
let (module, stats) = run(&text);
assert!(!stats.changed());
assert_eq!(count_of(one(&module), Opcode::Load), 1);
}
#[test]
fn one_arm_reading_it_is_not_the_other_arm_having_read_it() {
let text = wrap(
"(ptr, i1) -> i32",
"block0(%0: ptr, %1: i1):
br_if %1, block1, block2
block1:
%2 = load.i32 %0, align 4
jump block3(%2)
block2:
%3 = load.i32 %0, align 4
jump block3(%3)
block3(%4: i32):
return %4
",
);
let (module, stats) = run(&text);
assert!(!stats.changed());
assert_eq!(count_of(one(&module), Opcode::Load), 2);
}
#[test]
fn a_load_above_the_branch_reaches_both_arms() {
let text = wrap(
"(ptr, i1) -> i32",
"block0(%0: ptr, %1: i1):
%2 = load.i32 %0, align 4
br_if %1, block1, block2
block1:
%3 = load.i32 %0, align 4
jump block3(%3)
block2:
%4 = load.i32 %0, align 4
jump block3(%4)
block3(%5: i32):
%6 = add %2, %5
return %6
",
);
let (module, stats) = run(&text);
assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 2);
let func = one(&module);
off(func);
assert_eq!(count_of(func, Opcode::Load), 1);
}
#[test]
fn a_field_read_after_a_struct_assignment_comes_out_of_the_source() {
let text = wrap(
"() -> i32",
"block0:
%0 = alloca, size 32, align 8
%1 = alloca, size 16, align 8
%2 = iconst.i64 12
%3 = ptr_add %0, %2
%4 = iconst.i32 7
store %4 -> %3, align 4
%5 = iconst.i64 8
%6 = ptr_add %0, %5
memcpy %1, %6, size 8, align 8
%7 = iconst.i64 4
%8 = ptr_add %1, %7
%9 = load.i32 %8, align 4
return %9
",
);
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);
assert_eq!(
count_of(func, Opcode::Memcpy),
1,
"the copy itself is not this pass's to remove"
);
}
#[test]
fn the_plane_writes_a_copy_leaves_behind_do_not_stop_the_walk() {
let text = wrap(
"() -> i32",
"block0:
%0 = alloca, size 32, align 8
%1 = alloca, size 16, align 8
%2 = iconst.i64 12
%3 = ptr_add %0, %2
%4 = iconst.i32 7
store %4 -> %3, align 4
%5 = iconst.i64 8
%6 = ptr_add %0, %5
memcpy %1, %6, size 8, align 8
%7 = iconst.i64 8
meta_type_copy %1, %6, %7
meta_init_copy %1, %6, %7
cap_copy %1, %6, %7
%8 = iconst.i64 4
%9 = ptr_add %1, %8
%10 = load.i32 %9, align 4
return %10
",
);
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);
}
#[test]
fn what_the_copy_moved_is_what_the_source_held_when_it_ran() {
let text = wrap(
"() -> i32",
"block0:
%0 = alloca, size 4, align 4
%1 = alloca, size 4, align 4
%2 = iconst.i32 7
store %2 -> %0, align 4
memcpy %1, %0, size 4, align 4
%3 = iconst.i32 9
store %3 -> %0, align 4
%4 = load.i32 %1, align 4
return %4
",
);
let (module, stats) = run(&text);
assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
let func = one(&module);
assert_eq!(count_of(func, Opcode::IConst), 2);
let ret = func
.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
.find(|&inst| func[inst].opcode == Opcode::Return)
.expect("a return");
let seven = func
.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
.find(|&inst| func[inst].opcode == Opcode::IConst)
.expect("the first constant, which is the one stored before the copy");
assert_eq!(func[func[ret].args][0], func[seven].results().next().expect("a result"));
}
#[test]
fn a_load_across_the_edge_of_a_copy_stays() {
let text = wrap(
"() -> i32",
"block0:
%0 = alloca, size 8, align 4
%1 = alloca, size 8, align 4
%2 = iconst.i32 7
store %2 -> %0, align 4
memcpy %1, %0, size 4, align 4
%3 = iconst.i64 2
%4 = ptr_add %1, %3
%5 = load.i32 %4, align 2
return %5
",
);
let (module, stats) = run(&text);
assert!(!stats.changed());
assert_eq!(count_of(one(&module), Opcode::Load), 1);
}
#[test]
fn a_copy_the_oracle_could_not_tell_apart_from_the_load_is_not_followed() {
let text = wrap(
"(ptr, ptr, ptr) -> i32",
"block0(%0: ptr, %1: ptr, %2: ptr):
%3 = iconst.i32 7
store %3 -> %1, align 4
memcpy %0, %1, size 4, align 4
%4 = load.i32 %2, align 4
return %4
",
);
let (module, stats) = run(&text);
assert!(!stats.changed());
assert_eq!(count_of(one(&module), Opcode::Load), 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);
}
#[test]
fn the_safety_instrumentation_between_them_does_not_stop_the_forward() {
let text = wrap(
"(ptr, i1) -> i32",
"block0(%0: ptr, %1: i1):
%2 = iconst.i32 7
store %2 -> %0, align 4
%3 = iconst.i64 4
meta_init %0, %3
br_if %1, block1, block2
block1:
%4 = cap_of %0
check_bounds %4, %0, size 4, align 4
jump block2
block2:
%5 = load.i32 %0, align 4
return %5
",
);
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);
assert_eq!(count_of(func, Opcode::MetaInit), 1);
assert_eq!(count_of(func, Opcode::CheckBounds), 1);
}
}