use rucc_ir::{Block, Def, Extra, Flags, Func, Inst, MemOrder, Opcode};
use crate::uses::{count, operands};
use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
const REMOVED: &str = "instruction with no effects and no users removed";
const NO_FUEL: &str = "dead instruction kept, the pass ran out of fuel";
const NEEDS_MEMORY_ANALYSIS: &str =
"instruction with effects left alone, removing it needs a memory analysis";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Dce;
impl Pass for Dce {
fn name(&self) -> &'static str {
"dce"
}
fn describe(&self) -> &'static str {
"an instruction with no effects whose results nothing uses is removed"
}
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 uses = count(func);
let mut work: Vec<Inst> = Vec::new();
for block in func.blocks().collect::<Vec<Block>>() {
for inst in func.insts(block) {
match verdict(func, inst, &uses) {
Verdict::Dead => work.push(inst),
Verdict::Effects => stats.missed(NEEDS_MEMORY_ANALYSIS),
Verdict::Used | Verdict::Terminator => {}
}
}
}
while let Some(inst) = work.pop() {
if func.block_of(inst).is_none() {
continue;
}
if verdict(func, inst, &uses) != Verdict::Dead {
continue;
}
if !fuel.take() {
stats.missed(NO_FUEL);
continue;
}
operands(func, inst, |value| {
let count = &mut uses[value.index()];
*count -= 1;
if *count == 0 {
if let Def::Result { inst: def, .. } = func[value].def {
work.push(def);
}
}
});
func.remove_inst(inst);
stats.optimized(REMOVED);
}
stats
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Verdict {
Dead,
Used,
Terminator,
Effects,
}
fn reads_only(func: &Func, inst: Inst) -> bool {
let data = &func[inst];
if data.opcode != Opcode::Load || data.flags.contains(Flags::VOLATILE) {
return false;
}
let Extra::Mem(mem) = data.extra else { return false };
func[mem].order == MemOrder::NotAtomic
}
fn verdict(func: &Func, inst: Inst, uses: &[u32]) -> Verdict {
let data = &func[inst];
if func.is_terminator(inst) {
return Verdict::Terminator;
}
if !data.results().all(|value| uses[value.index()] == 0) {
return Verdict::Used;
}
if data.opcode.has_effects() && !reads_only(func, inst) {
return Verdict::Effects;
}
debug_assert!(
data.opcode != Opcode::InlineAsm,
"inline assembly has effects and cannot reach here"
);
Verdict::Dead
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Block, Builder, Flags, Func, MemInfo, MemOrder, Opcode, Restrict, Signature, Type,
};
use crate::stats::Kind;
use crate::{Analysis, Fuel, Pass, dce::Dce};
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(32)]));
let block = func.create_block();
(names, func, block)
}
fn plain(order: MemOrder) -> MemInfo {
MemInfo { size: 4, align: 4, owns: 4, order, tbaa: None, restrict: Restrict::NONE }
}
fn left(func: &Func, block: Block) -> usize {
func.insts(block).count()
}
#[test]
fn arithmetic_nothing_reads_goes_away() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let a = build.iconst(Type::int(32), 2);
let b = build.iconst(Type::int(32), 3);
build.binary(Opcode::Add, a, b, Flags::NONE);
build.ret(&[a]);
assert!(
Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
assert_eq!(left(&func, block), 2);
}
#[test]
fn the_counts_in_the_cache_go_with_the_uses_that_were_removed() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let a = build.iconst(Type::int(32), 2);
let b = build.iconst(Type::int(32), 3);
build.binary(Opcode::Add, a, b, Flags::NONE);
build.ret(&[a]);
let mut an = crate::machine::fixtures::analyses();
an.pressure(&func);
assert!(Dce.run(&mut func, &mut an, &mut Fuel::unlimited()).changed());
assert!(an.settle(&func, Dce.preserves(), true).is_empty(), "the pass was caught out");
assert!(!an.holds(Analysis::Pressure), "a stale count was left for the next pass to read");
}
#[test]
fn arithmetic_something_reads_stays() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let a = build.iconst(Type::int(32), 2);
let b = build.iconst(Type::int(32), 3);
let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
build.ret(&[sum]);
assert!(
!Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
assert_eq!(left(&func, block), 4);
}
#[test]
fn a_value_used_twice_is_not_dead_when_one_use_goes() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let x = build.iconst(Type::int(32), 7);
let kept = build.binary(Opcode::Add, x, x, Flags::NONE);
build.binary(Opcode::Add, x, x, Flags::NONE);
build.ret(&[kept]);
assert!(
Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
assert_eq!(left(&func, block), 3);
}
#[test]
fn a_store_stays_however_dead_it_looks() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let value = build.iconst(Type::int(32), 1);
let address = build.iconst(Type::int(64), 0);
let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
let info = MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
build.store(value, address, info, Flags::NONE);
build.ret(&[value]);
let stats =
Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
assert!(!stats.changed());
assert_eq!(left(&func, block), 5);
assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
}
#[test]
fn a_load_nothing_reads_goes_away() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let address = build.iconst(Type::int(64), 0);
let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
build.load(Type::int(32), address, plain(MemOrder::NotAtomic), Flags::NONE);
let kept = build.iconst(Type::int(32), 1);
build.ret(&[kept]);
let stats =
Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
assert!(stats.changed());
assert_eq!(left(&func, block), 2);
assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 0);
}
#[test]
fn a_volatile_load_nothing_reads_stays() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let address = build.iconst(Type::int(64), 0);
let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
build.load(Type::int(32), address, plain(MemOrder::NotAtomic), Flags::VOLATILE);
let kept = build.iconst(Type::int(32), 1);
build.ret(&[kept]);
let stats =
Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
assert!(!stats.changed());
assert_eq!(left(&func, block), 5);
assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
}
#[test]
fn an_atomic_load_nothing_reads_stays_however_weak_it_is() {
for order in [MemOrder::Relaxed, MemOrder::Acquire, MemOrder::SeqCst] {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let address = build.iconst(Type::int(64), 0);
let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
build.load(Type::int(32), address, plain(order), Flags::NONE);
let kept = build.iconst(Type::int(32), 1);
build.ret(&[kept]);
let stats = Dce.run(
&mut func,
&mut crate::machine::fixtures::analyses(),
&mut Fuel::unlimited(),
);
assert!(!stats.changed(), "{order:?}");
assert_eq!(left(&func, block), 5, "{order:?}");
}
}
#[test]
fn a_value_a_branch_passes_on_is_used_by_the_branch() {
let (_, mut func, block) = blank();
let target = func.create_block();
let param = func.append_param(target, Type::int(32));
let mut build = Builder::new(&mut func, block);
let x = build.iconst(Type::int(32), 9);
build.jump(target, &[x]);
let mut build = Builder::new(&mut func, target);
build.ret(&[param]);
assert!(
!Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
assert_eq!(left(&func, block), 2);
}
#[test]
fn a_result_a_removed_instruction_read_is_looked_at_again() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let a = build.iconst(Type::int(32), 2);
let b = build.iconst(Type::int(32), 3);
let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
let doubled = build.binary(Opcode::Add, sum, sum, Flags::NONE);
build.unary(Opcode::SExt, doubled, Type::int(64));
let kept = build.iconst(Type::int(32), 1);
build.ret(&[kept]);
assert!(
Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
assert_eq!(left(&func, block), 2);
}
#[test]
fn fuel_stops_the_removing_and_not_the_looking() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let a = build.iconst(Type::int(32), 2);
let b = build.iconst(Type::int(32), 3);
build.binary(Opcode::Add, a, b, Flags::NONE);
build.ret(&[a]);
let mut fuel = Fuel::of(1);
let stats = Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut fuel);
assert!(stats.changed());
assert_eq!(left(&func, block), 3);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
}
}