use std::collections::HashMap;
use rucc_base::Symbol;
use rucc_ir::{Block, Extra, Flags, FloatPred, Func, Inst, IntPred, Opcode, Type, Value};
use crate::uses::substitute;
use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
const ADDRESS: &str = "address removed, an earlier one in the block computes the same address";
const MERGED: &str = "instruction removed, an earlier one in the block computes the same thing";
const NO_FUEL: &str = "duplicate instruction kept, the pass ran out of fuel";
const OPERANDS: usize = 3;
#[derive(Debug)]
pub struct Number;
impl Pass for Number {
fn name(&self) -> &'static str {
"number"
}
fn describe(&self) -> &'static str {
"two instructions in a block computing the same thing from the same things are one 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 same: HashMap<Value, Value> = HashMap::new();
let mut gone: Vec<Inst> = Vec::new();
for block in func.blocks().collect::<Vec<Block>>() {
let mut seen: HashMap<Key, Value> = HashMap::new();
for inst in func.insts(block).collect::<Vec<Inst>>() {
let Some((key, result)) = key(func, &same, inst) else { continue };
let Some(&first) = seen.get(&key) else {
seen.insert(key, result);
continue;
};
if !fuel.take() {
stats.missed(NO_FUEL);
continue;
}
same.insert(result, first);
gone.push(inst);
stats.optimized(if is_address(func[inst].opcode) { ADDRESS } else { MERGED });
}
}
for inst in gone {
func.remove_inst(inst);
}
if !same.is_empty() {
substitute(func, &same);
}
stats
}
}
fn is_address(opcode: Opcode) -> bool {
matches!(opcode, Opcode::PtrAdd | Opcode::GlobalAddr)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct Key {
opcode: Opcode,
flags: Flags,
ty: Type,
tag: Tag,
args: [Option<Value>; OPERANDS],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum Tag {
None,
Bits(u128),
Symbol(Symbol),
IntPred(IntPred),
FloatPred(FloatPred),
}
fn key(func: &Func, same: &HashMap<Value, Value>, inst: Inst) -> Option<(Key, Value)> {
let data = &func[inst];
if data.opcode.has_effects() || data.opcode == Opcode::MemEntry {
return None;
}
let mut results = data.results();
let (Some(result), None) = (results.next(), results.next()) else { return None };
let tag = match data.extra {
Extra::None => Tag::None,
Extra::Imm(at) => Tag::Bits(func[at].bits()),
Extra::Symbol(name) => Tag::Symbol(name),
Extra::IntPred(pred) => Tag::IntPred(pred),
Extra::FloatPred(pred) => Tag::FloatPred(pred),
_ => return None,
};
let operands = &func[data.args];
if operands.len() > OPERANDS {
return None;
}
let mut args = [None; OPERANDS];
for (slot, &arg) in args.iter_mut().zip(operands) {
*slot = Some(same.get(&arg).copied().unwrap_or(arg));
}
if data.opcode.is_commutative() && operands.len() == 2 {
args[..2].sort_unstable();
}
Some((Key { opcode: data.opcode, flags: data.flags, ty: func[result].ty, tag, args }, result))
}
#[cfg(test)]
mod tests {
use rucc_ir::{
Block, Builder, Def, Extra, InstData, MemInfo, MemOrder, Restrict, Signature, Type,
};
use super::*;
use crate::stats::Kind;
fn blank() -> (Func, Block) {
let mut names = rucc_base::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();
(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: 32, ..plain(8) });
build.value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR)
}
fn run(func: &mut Func) -> Stats {
Number.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
}
fn count(func: &Func, opcode: Opcode) -> usize {
func.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
.filter(|&inst| func[inst].opcode == opcode)
.count()
}
fn returned(func: &Func) -> Vec<Value> {
let block = func.blocks().last().expect("the function has a block");
let inst = func.terminator(block).expect("the block has a terminator");
func[func[inst].args].to_vec()
}
fn operands(func: &Func, value: Value) -> Vec<Value> {
let Def::Result { inst, .. } = func[value].def else { panic!("not an instruction result") };
func[func[inst].args].to_vec()
}
#[test]
fn the_same_arithmetic_on_the_same_operands_twice_is_one_instruction() {
let (mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let left = build.iconst(Type::int(64), 3);
let right = build.iconst(Type::int(64), 5);
let first = build.binary(Opcode::Add, left, right, Flags::NONE);
let second = build.binary(Opcode::Add, left, right, Flags::NONE);
build.ret(&[first, second]);
let stats = run(&mut func);
assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
assert_eq!(count(&func, Opcode::Add), 1);
assert_eq!(returned(&func), vec![first, first]);
}
#[test]
fn a_commutative_pair_matches_with_its_operands_the_other_way_round() {
let (mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let left = build.iconst(Type::int(64), 3);
let right = build.iconst(Type::int(64), 5);
let first = build.binary(Opcode::Add, left, right, Flags::NONE);
let second = build.binary(Opcode::Add, right, left, Flags::NONE);
build.ret(&[first, second]);
let stats = run(&mut func);
assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
assert_eq!(returned(&func), vec![first, first]);
}
#[test]
fn a_subtraction_the_other_way_round_is_a_different_answer() {
let (mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let left = build.iconst(Type::int(64), 3);
let right = build.iconst(Type::int(64), 5);
let first = build.binary(Opcode::Sub, left, right, Flags::NONE);
let second = build.binary(Opcode::Sub, right, left, Flags::NONE);
build.ret(&[first, second]);
let stats = run(&mut func);
assert!(!stats.changed(), "three minus five is not five minus three");
assert_eq!(count(&func, Opcode::Sub), 2);
}
#[test]
fn two_adds_that_promise_different_things_stay_two_adds() {
let (mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let left = build.iconst(Type::int(64), 3);
let right = build.iconst(Type::int(64), 5);
let first = build.binary(Opcode::Add, left, right, Flags::NSW);
let second = build.binary(Opcode::Add, left, right, Flags::NONE);
build.ret(&[first, second]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(count(&func, Opcode::Add), 2);
}
#[test]
fn a_chain_collapses_all_the_way_up_and_not_just_at_the_bottom() {
let (mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let index = build.iconst(Type::int(64), 2);
let scale = build.iconst(Type::int(64), 8);
let first = build.binary(Opcode::Mul, index, scale, Flags::NONE);
let second = build.binary(Opcode::Mul, index, scale, Flags::NONE);
let up = build.binary(Opcode::Add, first, scale, Flags::NONE);
let down = build.binary(Opcode::Add, second, scale, Flags::NONE);
build.ret(&[up, down]);
let stats = run(&mut func);
assert_eq!(stats.count(Kind::Optimized, MERGED), 2);
assert_eq!(count(&func, Opcode::Mul), 1);
assert_eq!(count(&func, Opcode::Add), 1);
assert_eq!(returned(&func), vec![up, up]);
}
#[test]
fn the_same_constant_written_twice_is_one_constant() {
let (mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let first = build.iconst(Type::int(64), 7);
let second = build.iconst(Type::int(64), 7);
let narrow = build.iconst(Type::int(32), 7);
build.ret(&[first, second, narrow]);
let stats = run(&mut func);
assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
assert_eq!(count(&func, Opcode::IConst), 2);
assert_eq!(returned(&func), vec![first, first, narrow]);
}
#[test]
fn two_allocas_are_two_addresses_however_alike_they_look() {
let (mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let one = local(&mut build);
let two = local(&mut build);
build.ret(&[one, two]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(count(&func, Opcode::Alloca), 2);
}
#[test]
fn two_loads_of_one_address_are_left_to_the_pass_that_knows_about_memory() {
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.ret(&[first, second]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(count(&func, Opcode::Load), 2);
}
#[test]
fn what_one_block_computes_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 left = build.iconst(Type::int(64), 3);
let right = build.iconst(Type::int(64), 5);
let first = build.binary(Opcode::Add, left, right, Flags::NONE);
build.jump(next, &[]);
let mut build = Builder::new(&mut func, next);
let second = build.binary(Opcode::Add, left, right, Flags::NONE);
build.ret(&[first, second]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(count(&func, Opcode::Add), 2);
}
#[test]
fn one_name_for_the_address_is_what_lets_the_load_be_forwarded() {
let (mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let base = local(&mut build);
let index = build.iconst(Type::int(64), 2);
let scale = build.iconst(Type::int(64), 8);
let wrote = build.iconst(Type::int(64), 7);
let to = build.binary(Opcode::Mul, index, scale, Flags::NONE);
let to = build.binary(Opcode::PtrAdd, base, to, Flags::NONE);
build.store(wrote, to, plain(8), Flags::NONE);
let from = build.binary(Opcode::Mul, index, scale, Flags::NONE);
let from = build.binary(Opcode::PtrAdd, base, from, Flags::NONE);
let read = build.load(Type::int(64), from, plain(8), Flags::NONE);
build.ret(&[read]);
let stats = run(&mut func);
assert_eq!(stats.count(Kind::Optimized, ADDRESS), 1);
assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
assert_eq!(count(&func, Opcode::PtrAdd), 1);
let mut analyses = crate::machine::fixtures::analyses();
let stats = crate::load::LoadForward.run(&mut func, &mut analyses, &mut Fuel::unlimited());
assert!(stats.changed(), "the two addresses are one value now");
assert_eq!(count(&func, Opcode::Load), 0);
assert_eq!(returned(&func), vec![wrote]);
}
#[test]
fn an_instruction_with_three_operands_is_matched_on_all_three() {
let (mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let left = build.iconst(Type::int(64), 3);
let right = build.iconst(Type::int(64), 5);
let which = build.icmp(IntPred::Slt, left, right);
let args = build.func().push_values(&[which, left, right]);
let pick = InstData { args, ..InstData::new(Opcode::Select) };
let first = build.value(pick, Type::int(64));
let second = build.value(pick, Type::int(64));
let args = build.func().push_values(&[which, right, left]);
let other = InstData { args, ..InstData::new(Opcode::Select) };
let other = build.value(other, Type::int(64));
build.ret(&[first, second, other]);
let stats = run(&mut func);
assert_eq!(stats.count(Kind::Optimized, MERGED), 1, "the arms the other way round differ");
assert_eq!(count(&func, Opcode::Select), 2);
assert_eq!(returned(&func), vec![first, first, other]);
}
#[test]
fn a_repeated_global_address_is_counted_as_an_address() {
let (mut func, block) = blank();
let mut names = rucc_base::Interner::new();
let global = names.intern("g");
let mut build = Builder::new(&mut func, block);
let named = InstData { extra: Extra::Symbol(global), ..InstData::new(Opcode::GlobalAddr) };
let first = build.value(named, Type::PTR);
let second = build.value(named, Type::PTR);
let offset = build.iconst(Type::int(64), 8);
let one = build.binary(Opcode::PtrAdd, first, offset, Flags::NONE);
let two = build.binary(Opcode::PtrAdd, second, offset, Flags::NONE);
build.ret(&[one, two]);
let stats = run(&mut func);
assert_eq!(stats.count(Kind::Optimized, ADDRESS), 2);
assert_eq!(count(&func, Opcode::GlobalAddr), 1);
assert_eq!(operands(&func, one), vec![first, offset]);
}
#[test]
fn without_fuel_the_duplicate_stays_and_the_chance_is_still_counted() {
let (mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let left = build.iconst(Type::int(64), 3);
let right = build.iconst(Type::int(64), 5);
let first = build.binary(Opcode::Add, left, right, Flags::NONE);
let second = build.binary(Opcode::Add, left, right, Flags::NONE);
build.ret(&[first, second]);
let stats =
Number.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::of(0));
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, NO_FUEL), 1);
assert_eq!(count(&func, Opcode::Add), 2);
}
}