use rucc_cost::heuristics;
use rucc_ir::{Block, Builder, Extra, Flags, Func, Inst, InstData, MemOrder, Opcode, Type, Value};
use crate::cfg::Cfg;
use crate::fold::constant;
use crate::profile::Probability;
use crate::simplify_cfg::{self, Bindings};
use crate::{Analyses, Fuel, Pass, Preserved, Stats};
const CONVERTED: &str =
"branch whose two arms only work out a value replaced by the value and no branch";
const FACTORED: &str = "operation both arms did to different operands done once below the branch";
const STORE_REPLACED: &str = "store both arms made to the same place made once below the branch";
const ARM_HAS_EFFECTS: &str =
"branch kept, an arm does something that only happens on the path it is on";
const STORE_ON_ONE_PATH: &str =
"branch kept, a store only one path makes would have to be made on the other path too";
const STORES_DO_NOT_MATCH: &str =
"branch kept, both paths store but not to one address the two of them name the same way";
const ARM_MAY_TRAP: &str = "branch kept, an arm divides and doing it on both paths could trap";
const NO_SELECT_AT_THAT_WIDTH: &str =
"branch kept, the value the arms disagree about is not a width a select is lowered at";
const ARMS_TOO_LONG: &str = "branch kept, its arms are more work than doing both of them is worth";
const BRANCH_IS_PREDICTED: &str =
"branch kept, it goes one way often enough that the machine will predict it";
const CONDITION_IS_DECIDED: &str =
"branch kept, its condition is already known and the arm that cannot run is better deleted";
const NO_FUEL: &str = "branch kept, the pass ran out of fuel";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PhiOpt;
impl Pass for PhiOpt {
fn name(&self) -> &'static str {
"phiopt"
}
fn describe(&self) -> &'static str {
"a branch whose two arms only work out a value becomes a select, and the branch goes"
}
fn preserves(&self) -> Preserved {
Preserved::NONE
}
fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
let mut stats = Stats::new();
if func.entry().is_none() {
return stats;
}
for head in func.blocks().collect::<Vec<Block>>() {
let cfg = an.cfg(func);
if !cfg.reaches(head) {
continue;
}
let Some(shape) = diamond(func, cfg, head) else { continue };
let store = storing(func, &shape);
if let Some(reason) = refused(func, &shape, store.as_ref()) {
stats.missed(reason);
continue;
}
let plan = factoring(func, &shape);
let replaced = plan.iter().flatten().count() + usize::from(store.is_some());
let saved = u32::try_from(replaced).unwrap_or(u32::MAX);
let work = shape
.arms
.map(|arm| arm.map_or(0, |block| length(func, block)).saturating_sub(saved));
if work.iter().any(|&count| count > 0) {
if work.iter().any(|&count| count > heuristics::PHIOPT_ARM_INSTRUCTIONS) {
stats.missed(ARMS_TOO_LONG);
continue;
}
if !unpredictable(an.frequencies(func).taken(head, 0)) {
stats.missed(BRANCH_IS_PREDICTED);
continue;
}
}
if !fuel.take() {
stats.missed(NO_FUEL);
break;
}
convert(func, &shape, &plan, store.as_ref());
an.clear();
for _ in plan.iter().flatten() {
stats.optimized(FACTORED);
}
if store.is_some() {
stats.optimized(STORE_REPLACED);
}
stats.optimized(CONVERTED);
}
stats
}
}
pub(crate) struct Diamond {
pub(crate) head: Block,
pub(crate) cond: Value,
pub(crate) join: Block,
pub(crate) arms: [Option<Block>; 2],
pub(crate) args: [Vec<Value>; 2],
}
pub(crate) fn diamond(func: &Func, cfg: &Cfg, head: Block) -> Option<Diamond> {
let entry = cfg.entry()?;
let term = func.terminator(head)?;
if func[term].opcode != Opcode::BrIf {
return None;
}
let cond = *func[func[term].args].first()?;
let mut targets = func.successors(term);
let sides = [targets.next()?, targets.next()?];
if sides[0].block == sides[1].block {
return None;
}
let through = [
passes_through(func, cfg, head, sides[0].block),
passes_through(func, cfg, head, sides[1].block),
];
let join = match through {
[Some(left), Some(right)] if left == right => left,
[Some(left), _] if left == sides[1].block => left,
[_, Some(right)] if right == sides[0].block => right,
_ => return None,
};
if join == head || join == entry {
return None;
}
let arms = [
(sides[0].block != join).then_some(sides[0].block),
(sides[1].block != join).then_some(sides[1].block),
];
let mut args = [Vec::new(), Vec::new()];
for (index, side) in sides.iter().enumerate() {
let carried = match arms[index] {
Some(arm) => func.successors(func.terminator(arm)?).next()?.args,
None => side.args,
};
args[index] = func[carried].to_vec();
}
Some(Diamond { head, cond, join, arms, args })
}
fn passes_through(func: &Func, cfg: &Cfg, head: Block, block: Block) -> Option<Block> {
if !func[block].params.is_empty() {
return None;
}
match cfg.predecessors(block) {
[only] if *only == head => {}
_ => return None,
}
let term = func.terminator(block)?;
if func[term].opcode != Opcode::Jump {
return None;
}
Some(func.successors(term).next()?.block)
}
fn refused(func: &Func, shape: &Diamond, store: Option<&Stored>) -> Option<&'static str> {
let term = func.terminator(shape.head).expect("the head of a diamond ends in its branch");
if simplify_cfg::taken(func, term, &Bindings::new()).is_some() {
return Some(CONDITION_IS_DECIDED);
}
let moving = store.map(|one| one.insts);
for &arm in shape.arms.iter().flatten() {
for inst in func.insts(arm) {
if func.is_terminator(inst) || moving.is_some_and(|two| two.contains(&inst)) {
continue;
}
if func[inst].opcode == Opcode::Store {
return Some(mismatch(func, shape));
}
if func[inst].opcode.has_effects() {
return Some(ARM_HAS_EFFECTS);
}
if !speculatable(func, inst) {
return Some(ARM_MAY_TRAP);
}
}
}
let params = func[shape.join].params.iter();
for ((¶m, &then), &other) in params.zip(&shape.args[0]).zip(&shape.args[1]) {
if agree(func, then, other) {
continue;
}
if !selectable(func[param].ty) {
return Some(NO_SELECT_AT_THAT_WIDTH);
}
}
None
}
fn agree(func: &Func, then: Value, other: Value) -> bool {
if then == other {
return true;
}
let (Some((left, lty)), Some((right, rty))) = (constant(func, then), constant(func, other))
else {
return false;
};
lty == rty && left == right
}
pub(crate) fn speculatable(func: &Func, inst: Inst) -> bool {
let opcode = func[inst].opcode;
if !matches!(opcode, Opcode::SDiv | Opcode::UDiv | Opcode::SRem | Opcode::URem) {
return true;
}
let Some(&divisor) = func[func[inst].args].get(1) else { return false };
let Some((imm, ty)) = constant(func, divisor) else { return false };
if imm.unsigned() == 0 {
return false;
}
imm.signed(ty) != -1
}
struct Stored {
insts: [Inst; 2],
values: [Value; 2],
addr: Value,
data: InstData,
}
fn mismatch(func: &Func, shape: &Diamond) -> &'static str {
let [Some(then), Some(other)] = shape.arms else { return STORE_ON_ONE_PATH };
match (stored_in(func, then), stored_in(func, other)) {
(Some(_), Some(_)) => STORES_DO_NOT_MATCH,
_ => STORE_ON_ONE_PATH,
}
}
fn storing(func: &Func, shape: &Diamond) -> Option<Stored> {
let [Some(then), Some(other)] = shape.arms else { return None };
let insts = [stored_in(func, then)?, stored_in(func, other)?];
let data = [func[insts[0]], func[insts[1]]];
if data[0].flags != data[1].flags || data[0].flags.contains(Flags::VOLATILE) {
return None;
}
let (Extra::Mem(one), Extra::Mem(two)) = (data[0].extra, data[1].extra) else { return None };
if func[one] != func[two] || func[one].order != MemOrder::NotAtomic {
return None;
}
let &[then, addr] = func[data[0].args].first_chunk::<2>()?;
let &[other, addr_two] = func[data[1].args].first_chunk::<2>()?;
if addr != addr_two || func[then].ty != func[other].ty {
return None;
}
if !agree(func, then, other) && !selectable(func[then].ty) {
return None;
}
Some(Stored { insts, values: [then, other], addr, data: data[0] })
}
fn stored_in(func: &Func, arm: Block) -> Option<Inst> {
let mut store = None;
for inst in func.insts(arm) {
if func.is_terminator(inst) || !func[inst].opcode.has_effects() {
continue;
}
if func[inst].opcode != Opcode::Store || store.is_some() {
return None;
}
store = Some(inst);
}
store
}
struct Factored {
insts: [Inst; 2],
operands: Vec<Value>,
differ: Option<(usize, [Value; 2])>,
data: InstData,
ty: Type,
}
fn factoring(func: &Func, shape: &Diamond) -> Vec<Option<Factored>> {
let count = shape.args[0].len();
let [Some(then), Some(other)] = shape.arms else {
return (0..count).map(|_| None).collect();
};
(0..count).map(|index| factored(func, shape, [then, other], index)).collect()
}
fn factored(func: &Func, shape: &Diamond, arms: [Block; 2], index: usize) -> Option<Factored> {
let sides = [shape.args[0][index], shape.args[1][index]];
if agree(func, sides[0], sides[1]) {
return None;
}
let insts = [written_in(func, arms[0], sides[0])?, written_in(func, arms[1], sides[1])?];
let data = [func[insts[0]], func[insts[1]]];
if data[0].opcode != data[1].opcode || data[0].flags != data[1].flags {
return None;
}
if data[0].extra != data[1].extra || func[sides[0]].ty != func[sides[1]].ty {
return None;
}
let operands = [func[data[0].args].to_vec(), func[data[1].args].to_vec()];
if operands[0].len() != operands[1].len() {
return None;
}
let mut apart =
operands[0].iter().zip(&operands[1]).enumerate().filter(|(_, (one, two))| one != two);
let differ = match (apart.next(), apart.next()) {
(_, Some(_)) => return None,
(Some((at, (&one, &two))), None) => {
if func[one].ty != func[two].ty || !selectable(func[one].ty) {
return None;
}
Some((at, [one, two]))
}
(None, None) => None,
};
let ty = func[sides[0]].ty;
Some(Factored { insts, operands: operands[0].clone(), differ, data: data[0], ty })
}
fn written_in(func: &Func, arm: Block, value: Value) -> Option<Inst> {
let inst = func
.insts(arm)
.find(|&inst| func[inst].results == 1 && func[inst].first_result == Some(value))?;
let mut seen = 0;
for inst in func.insts(arm) {
seen += func[func[inst].args].iter().filter(|&&arg| arg == value).count();
for call in func.successors(inst) {
seen += func[call.args].iter().filter(|&&arg| arg == value).count();
}
}
(seen == 1).then_some(inst)
}
fn selectable(ty: Type) -> bool {
ty.is_scalar() && ty.is_int() && matches!(ty.bits(), 8 | 16 | 32 | 64)
}
pub(crate) fn length(func: &Func, block: Block) -> u32 {
let count = func.insts(block).filter(|&inst| !func.is_terminator(inst)).count();
u32::try_from(count).unwrap_or(u32::MAX)
}
pub(crate) fn unpredictable(taken: Probability) -> bool {
let margin = heuristics::PHIOPT_UNPREDICTABLE_MARGIN_PERCENT * (Probability::SCALE / 100);
taken.parts() >= margin && taken.parts() <= Probability::SCALE - margin
}
fn convert(func: &mut Func, shape: &Diamond, plan: &[Option<Factored>], store: Option<&Stored>) {
let term = func.terminator(shape.head).expect("the head of a diamond ends in its branch");
let span = func.span(term);
func.remove_inst(term);
let mut dropped: Vec<Inst> = plan.iter().flatten().flat_map(|one| one.insts).collect();
dropped.extend(store.iter().flat_map(|one| one.insts));
for &arm in shape.arms.iter().flatten() {
for inst in func.insts(arm).collect::<Vec<Inst>>() {
if func.is_terminator(inst) {
continue;
}
func.remove_inst(inst);
if !dropped.contains(&inst) {
func.append_inst(shape.head, inst);
}
}
}
let mut build = Builder::new(func, shape.head).at(span);
let mut args = Vec::with_capacity(shape.args[0].len());
for (index, (&then, &other)) in shape.args[0].iter().zip(&shape.args[1]).enumerate() {
if let Some(one) = &plan[index] {
let mut operands = one.operands.clone();
if let Some((at, sides)) = one.differ {
operands[at] = build.select(shape.cond, sides[0], sides[1]);
}
let list = build.func().push_values(&operands);
args.push(build.value(InstData { args: list, ..one.data }, one.ty));
continue;
}
let same = agree(build.func(), then, other);
args.push(if same { then } else { build.select(shape.cond, then, other) });
}
if let Some(one) = store {
let [then, other] = one.values;
let same = agree(build.func(), then, other);
let what = if same { then } else { build.select(shape.cond, then, other) };
let list = build.func().push_values(&[what, one.addr]);
build.inst(InstData { args: list, ..one.data }, &[]);
}
build.jump(shape.join, &args);
for &arm in shape.arms.iter().flatten() {
func.remove_block(arm);
}
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Block, Builder, Flags, Func, IntPred, MemInfo, MemOrder, Opcode, Restrict, Signature, Type,
Value,
};
use super::PhiOpt;
use crate::profile::{Probability, Quality};
use crate::stats::Kind;
use crate::{Analyses, Fuel, Pass, Stats};
fn phiopt(func: &mut Func) -> Stats {
PhiOpt.run(func, &mut Analyses::new(), &mut Fuel::unlimited())
}
fn blocks(func: &Func) -> Vec<usize> {
func.blocks().map(Block::index).collect()
}
fn goes_to(func: &Func, block: usize) -> Vec<usize> {
let block = Block::from_usize(block);
let term = func.terminator(block).expect("every block here has one");
func.successors(term).map(|call| call.block.index()).collect()
}
fn opcodes(func: &Func, block: usize) -> Vec<Opcode> {
let block = Block::from_usize(block);
func.insts(block).map(|inst| func[inst].opcode).collect()
}
fn carries(func: &Func, block: usize) -> Vec<Value> {
let block = Block::from_usize(block);
let term = func.terminator(block).expect("every block here has one");
let call = func.successors(term).next().expect("a terminator here has an edge");
func[call.args].to_vec()
}
fn plain() -> MemInfo {
MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
}
}
fn store_something(build: &mut Builder<'_>) {
let what = build.iconst(Type::int(32), 7);
let address = build.iconst(Type::int(64), 16);
let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
build.store(what, address, plain(), Flags::NONE);
}
fn both_arms_store(info: MemInfo, flags: [Flags; 2], addresses: bool) -> Func {
let mut names = Interner::new();
let ints = [Type::PTR, Type::int(32), Type::int(32), Type::PTR];
let signature = Signature::new().with_params(&ints);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let address = func.append_param(head, Type::PTR);
let written =
[func.append_param(head, Type::int(32)), func.append_param(head, Type::int(32))];
let elsewhere = func.append_param(head, Type::PTR);
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let mut build = Builder::new(&mut func, head);
let zero = build.iconst(Type::int(32), 0);
let test = build.icmp(IntPred::Slt, written[0], zero);
build.br_if(test, arms[0], &[], arms[1], &[]);
for (index, arm) in arms.iter().enumerate() {
let mut build = Builder::new(&mut func, *arm);
let where_to = if addresses && index == 1 { elsewhere } else { address };
build.store(written[index], where_to, info, flags[index]);
build.jump(join, &[]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[]);
func
}
fn empty_arms() -> Func {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32), Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let left = func.append_param(head, Type::int(32));
let right = func.append_param(head, Type::int(32));
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, head);
let test = build.icmp(IntPred::Slt, left, right);
build.br_if(test, arms[0], &[], arms[1], &[]);
for (arm, value) in arms.iter().zip([1, 2]) {
let mut build = Builder::new(&mut func, *arm);
let it = build.iconst(Type::int(32), value);
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
func
}
#[test]
fn a_branch_that_is_already_decided_is_left_for_simplify_cfg() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let head = func.create_block();
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, head);
let one = build.iconst(Type::int(32), 1);
let zero = build.iconst(Type::int(32), 0);
let test = build.icmp(IntPred::Ne, one, zero);
build.br_if(test, arms[0], &[], arms[1], &[]);
for (arm, value) in arms.iter().zip([1, 2]) {
let mut build = Builder::new(&mut func, *arm);
let it = build.iconst(Type::int(32), value);
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Missed, super::CONDITION_IS_DECIDED), 1);
assert_eq!(blocks(&func), vec![0, 1, 2, 3]);
}
#[test]
fn a_diamond_whose_arms_are_empty_becomes_a_select() {
let mut func = empty_arms();
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
assert_eq!(
opcodes(&func, 0),
vec![Opcode::ICmp, Opcode::IConst, Opcode::IConst, Opcode::Select, Opcode::Jump]
);
assert_eq!(goes_to(&func, 0), vec![3]);
assert_eq!(blocks(&func), vec![0, 3]);
}
#[test]
fn the_side_the_condition_holds_on_is_the_side_the_select_takes_first() {
let mut func = empty_arms();
phiopt(&mut func);
let select = func
.insts(Block::from_usize(0))
.find(|&inst| func[inst].opcode == Opcode::Select)
.expect("the select the pass just built");
let args = func[func[select].args].to_vec();
let one = crate::fold::constant(&func, args[1]).expect("the true arm carried a constant");
let two = crate::fold::constant(&func, args[2]).expect("the false arm carried a constant");
assert_eq!(one.0.unsigned(), 1, "the arm the branch named first");
assert_eq!(two.0.unsigned(), 2, "the arm the branch named second");
}
#[test]
fn a_triangle_whose_empty_side_goes_straight_to_the_join_is_converted() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let outside = func.append_param(head, Type::int(32));
let arm = func.create_block();
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, head);
let zero = build.iconst(Type::int(32), 0);
let test = build.icmp(IntPred::Slt, outside, zero);
build.br_if(test, arm, &[], join, &[outside]);
let mut build = Builder::new(&mut func, arm);
let it = build.iconst(Type::int(32), 0);
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
assert_eq!(blocks(&func), vec![0, 2]);
assert_eq!(goes_to(&func, 0), vec![2]);
assert_eq!(opcodes(&func, 0).last(), Some(&Opcode::Jump));
}
#[test]
fn a_parameter_both_sides_agree_about_needs_no_select() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let outside = func.append_param(head, Type::int(32));
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, head);
let zero = build.iconst(Type::int(32), 0);
let test = build.icmp(IntPred::Slt, outside, zero);
build.br_if(test, arms[0], &[], arms[1], &[]);
for arm in arms {
let mut build = Builder::new(&mut func, arm);
build.jump(join, &[outside]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
assert!(!opcodes(&func, 0).contains(&Opcode::Select), "both sides carried the same value");
assert_eq!(carries(&func, 0), vec![outside]);
}
#[test]
fn two_sides_carrying_the_same_number_need_no_select_either() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let outside = func.append_param(head, Type::int(32));
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, head);
let zero = build.iconst(Type::int(32), 0);
let test = build.icmp(IntPred::Slt, outside, zero);
build.br_if(test, arms[0], &[], arms[1], &[]);
for arm in arms {
let mut build = Builder::new(&mut func, arm);
let seven = build.iconst(Type::int(32), 7);
build.jump(join, &[seven]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
assert!(!opcodes(&func, 0).contains(&Opcode::Select), "both sides carried a seven");
}
#[test]
fn two_sides_carrying_different_numbers_still_get_a_select() {
let mut func = empty_arms();
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
assert!(opcodes(&func, 0).contains(&Opcode::Select), "one and two are not the same number");
}
#[test]
fn a_store_both_arms_make_to_one_place_is_made_once_below_the_branch() {
let mut func = both_arms_store(plain(), [Flags::NONE; 2], false);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::STORE_REPLACED), 1);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
assert_eq!(
opcodes(&func, 0),
vec![Opcode::IConst, Opcode::ICmp, Opcode::Select, Opcode::Store, Opcode::Jump]
);
assert_eq!(blocks(&func), vec![0, 3]);
assert_eq!(goes_to(&func, 0), vec![3]);
}
#[test]
fn the_one_store_writes_what_the_side_the_condition_holds_on_was_writing() {
let mut func = both_arms_store(plain(), [Flags::NONE; 2], false);
phiopt(&mut func);
let head = Block::from_usize(0);
let select = func
.insts(head)
.find(|&inst| func[inst].opcode == Opcode::Select)
.expect("the select the pass just built");
let store = func
.insts(head)
.find(|&inst| func[inst].opcode == Opcode::Store)
.expect("the one store that is left");
let chosen = func[func[select].args].to_vec();
let written = func[func[store].args].to_vec();
let params = func[head].params.to_vec();
assert_eq!(chosen[1], params[1], "the arm the branch named first");
assert_eq!(chosen[2], params[2], "the arm the branch named second");
assert_eq!(written[0], func[select].first_result.expect("a select produces one value"));
assert_eq!(written[1], params[0], "the address both arms named");
}
#[test]
fn two_arms_that_write_the_same_thing_get_a_store_and_no_select() {
let mut func = both_arms_store(plain(), [Flags::NONE; 2], false);
let head = Block::from_usize(0);
let params = func[head].params.to_vec();
let store = func
.insts(Block::from_usize(2))
.find(|&inst| func[inst].opcode == Opcode::Store)
.expect("the second arm's store");
let args = func.push_values(&[params[1], params[0]]);
func[store].args = args;
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::STORE_REPLACED), 1);
assert_eq!(
opcodes(&func, 0),
vec![Opcode::IConst, Opcode::ICmp, Opcode::Store, Opcode::Jump]
);
}
#[test]
fn two_arms_that_store_to_different_addresses_keep_their_branch() {
let mut func = both_arms_store(plain(), [Flags::NONE; 2], true);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::STORE_REPLACED), 0);
assert_eq!(stats.count(Kind::Missed, super::STORES_DO_NOT_MATCH), 1);
assert_eq!(goes_to(&func, 0), vec![1, 2]);
}
#[test]
fn a_volatile_store_keeps_its_branch_even_when_both_arms_make_it() {
let mut func = both_arms_store(plain(), [Flags::VOLATILE; 2], false);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::STORE_REPLACED), 0);
assert_eq!(stats.count(Kind::Missed, super::STORES_DO_NOT_MATCH), 1);
assert_eq!(goes_to(&func, 0), vec![1, 2]);
}
#[test]
fn an_atomic_store_keeps_its_branch_even_when_both_arms_make_it() {
let mut func = both_arms_store(
MemInfo { order: MemOrder::SeqCst, ..plain() },
[Flags::NONE; 2],
false,
);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::STORE_REPLACED), 0);
assert_eq!(stats.count(Kind::Missed, super::STORES_DO_NOT_MATCH), 1);
}
#[test]
fn two_stores_that_disagree_about_the_access_keep_their_branch() {
let mut func = both_arms_store(plain(), [Flags::NONE; 2], false);
let store = func
.insts(Block::from_usize(2))
.find(|&inst| func[inst].opcode == Opcode::Store)
.expect("the second arm's store");
let mem = func.add_mem(MemInfo { align: 1, ..plain() });
func[store].extra = rucc_ir::Extra::Mem(mem);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::STORE_REPLACED), 0);
assert_eq!(stats.count(Kind::Missed, super::STORES_DO_NOT_MATCH), 1);
}
#[test]
fn a_store_each_way_does_not_count_against_how_long_the_arms_may_be() {
let mut func = both_arms_store(plain(), [Flags::NONE; 2], false);
let params = func[Block::from_usize(0)].params.to_vec();
for arm in [1, 2] {
let block = Block::from_usize(arm);
let term = func.terminator(block).expect("an arm ends in its jump");
func.remove_inst(term);
let mut build = Builder::new(&mut func, block);
let mut value = params[1];
for _ in 0..rucc_cost::heuristics::PHIOPT_ARM_INSTRUCTIONS {
value = build.binary(Opcode::Add, value, params[2], Flags::NONE);
}
func.append_inst(block, term);
}
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Missed, super::ARMS_TOO_LONG), 0);
assert_eq!(stats.count(Kind::Optimized, super::STORE_REPLACED), 1);
}
#[test]
fn an_arm_that_stores_where_the_other_does_not_keeps_its_branch() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let outside = func.append_param(head, Type::int(32));
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, head);
let zero = build.iconst(Type::int(32), 0);
let test = build.icmp(IntPred::Slt, outside, zero);
build.br_if(test, arms[0], &[], arms[1], &[]);
let mut build = Builder::new(&mut func, arms[0]);
store_something(&mut build);
let it = build.iconst(Type::int(32), 1);
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, arms[1]);
let it = build.iconst(Type::int(32), 2);
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 0);
assert_eq!(stats.count(Kind::Missed, super::STORE_ON_ONE_PATH), 1);
assert_eq!(goes_to(&func, 0), vec![1, 2]);
}
#[test]
fn an_arm_that_does_something_else_keeps_its_branch() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let outside = func.append_param(head, Type::int(32));
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, head);
let zero = build.iconst(Type::int(32), 0);
let test = build.icmp(IntPred::Slt, outside, zero);
build.br_if(test, arms[0], &[], arms[1], &[]);
let mut build = Builder::new(&mut func, arms[0]);
let address = build.iconst(Type::int(64), 16);
let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
let it = build.load(Type::int(32), address, plain(), Flags::NONE);
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, arms[1]);
let it = build.iconst(Type::int(32), 2);
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 0);
assert_eq!(stats.count(Kind::Missed, super::ARM_HAS_EFFECTS), 1);
assert_eq!(goes_to(&func, 0), vec![1, 2]);
}
#[test]
fn an_arm_that_divides_by_something_unknown_keeps_its_branch() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32), Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let left = func.append_param(head, Type::int(32));
let right = func.append_param(head, Type::int(32));
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, head);
let zero = build.iconst(Type::int(32), 0);
let test = build.icmp(IntPred::Ne, right, zero);
build.br_if(test, arms[0], &[], arms[1], &[]);
let mut build = Builder::new(&mut func, arms[0]);
let it = build.binary(Opcode::SDiv, left, right, Flags::NONE);
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, arms[1]);
let it = build.iconst(Type::int(32), 0);
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 0);
assert_eq!(stats.count(Kind::Missed, super::ARM_MAY_TRAP), 1);
assert_eq!(goes_to(&func, 0), vec![1, 2]);
}
#[test]
fn a_division_by_a_constant_that_is_not_zero_or_minus_one_is_moved() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let outside = func.append_param(head, Type::int(32));
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, head);
let zero = build.iconst(Type::int(32), 0);
let test = build.icmp(IntPred::Slt, outside, zero);
build.br_if(test, arms[0], &[], arms[1], &[]);
let mut build = Builder::new(&mut func, arms[0]);
let three = build.iconst(Type::int(32), 3);
let it = build.binary(Opcode::SDiv, outside, three, Flags::NONE);
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, arms[1]);
let it = build.iconst(Type::int(32), 0);
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
assert!(opcodes(&func, 0).contains(&Opcode::SDiv));
}
#[test]
fn a_value_no_select_is_lowered_for_keeps_its_branch() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let outside = func.append_param(head, Type::int(32));
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
func.append_param(join, Type::PTR);
let mut build = Builder::new(&mut func, head);
let zero = build.iconst(Type::int(32), 0);
let test = build.icmp(IntPred::Slt, outside, zero);
build.br_if(test, arms[0], &[], arms[1], &[]);
for (arm, value) in arms.iter().zip([16, 32]) {
let mut build = Builder::new(&mut func, *arm);
let it = build.iconst(Type::int(64), value);
let it = build.unary(Opcode::IntToPtr, it, Type::PTR);
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 0);
assert_eq!(stats.count(Kind::Missed, super::NO_SELECT_AT_THAT_WIDTH), 1);
}
#[test]
fn arms_with_more_work_in_them_than_the_budget_keep_their_branch() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let outside = func.append_param(head, Type::int(32));
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, head);
let zero = build.iconst(Type::int(32), 0);
let test = build.icmp(IntPred::Slt, outside, zero);
build.br_if(test, arms[0], &[], arms[1], &[]);
let mut build = Builder::new(&mut func, arms[0]);
let mut it = outside;
for _ in 0..4 {
it = build.binary(Opcode::Add, it, outside, Flags::NONE);
}
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, arms[1]);
let it = build.iconst(Type::int(32), 0);
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 0);
assert_eq!(stats.count(Kind::Missed, super::ARMS_TOO_LONG), 1);
}
#[test]
fn the_margin_is_a_quarter_in_from_each_end() {
let guessed = |percent: u32| Probability::percent(percent, Quality::Guessed);
assert!(super::unpredictable(Probability::even()));
assert!(super::unpredictable(guessed(25)));
assert!(super::unpredictable(guessed(75)));
assert!(!super::unpredictable(guessed(24)));
assert!(!super::unpredictable(guessed(76)));
assert!(!super::unpredictable(Probability::always()));
assert!(!super::unpredictable(Probability::never()));
}
#[test]
fn an_arm_that_two_edges_reach_is_not_an_arm() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let outside = func.append_param(head, Type::int(32));
let above = func.create_block();
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, head);
let zero = build.iconst(Type::int(32), 0);
let first = build.icmp(IntPred::Slt, outside, zero);
build.br_if(first, above, &[], arms[0], &[]);
let mut build = Builder::new(&mut func, above);
let one = build.iconst(Type::int(32), 1);
let second = build.icmp(IntPred::Slt, outside, one);
build.br_if(second, arms[0], &[], arms[1], &[]);
for (arm, value) in arms.iter().zip([1, 2]) {
let mut build = Builder::new(&mut func, *arm);
let it = build.iconst(Type::int(32), value);
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 0);
assert_eq!(goes_to(&func, 0), vec![1, 2]);
assert_eq!(goes_to(&func, 1), vec![2, 3]);
}
#[test]
fn fuel_stops_the_conversion_where_it_stands() {
let mut func = empty_arms();
let mut fuel = Fuel::of(0);
let stats = PhiOpt.run(&mut func, &mut Analyses::new(), &mut fuel);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 0);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
assert_eq!(goes_to(&func, 0), vec![1, 2]);
}
fn same_operation(steps: &[Opcode]) -> Func {
let mut names = Interner::new();
let int = Type::int(32);
let signature = Signature::new().with_params(&[int, int, int, int]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let left = func.append_param(head, int);
let right = func.append_param(head, int);
let operands = [func.append_param(head, int), func.append_param(head, int)];
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let params: Vec<Value> = steps.iter().map(|_| func.append_param(join, int)).collect();
let mut build = Builder::new(&mut func, head);
let shared = build.iconst(int, 3);
let test = build.icmp(IntPred::Slt, left, right);
build.br_if(test, arms[0], &[], arms[1], &[]);
for (&arm, operand) in arms.iter().zip(operands) {
let mut build = Builder::new(&mut func, arm);
let carried: Vec<Value> = steps
.iter()
.map(|&opcode| build.binary(opcode, operand, shared, Flags::default()))
.collect();
build.jump(join, &carried);
}
let mut build = Builder::new(&mut func, join);
build.ret(¶ms);
func
}
#[test]
fn an_operation_both_arms_did_is_done_once_below_the_branch() {
let mut func = same_operation(&[Opcode::Add]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FACTORED), 1);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
assert_eq!(
opcodes(&func, 0),
vec![Opcode::IConst, Opcode::ICmp, Opcode::Select, Opcode::Add, Opcode::Jump],
"the select chooses the operand and the add happens once"
);
assert_eq!(blocks(&func), vec![0, 3]);
}
#[test]
fn the_select_chooses_the_operands_and_not_the_answers() {
let mut func = same_operation(&[Opcode::Add]);
phiopt(&mut func);
let head = Block::from_usize(0);
let select = func
.insts(head)
.find(|&inst| func[inst].opcode == Opcode::Select)
.expect("the select the pass just built");
let add = func
.insts(head)
.find(|&inst| func[inst].opcode == Opcode::Add)
.expect("the add the pass just wrote");
let chosen = func[func[select].args].to_vec();
let params = func[head].params.to_vec();
assert_eq!(&chosen[1..], ¶ms[2..], "the two operands the arms differed in");
let added = func[func[add].args].to_vec();
assert_eq!(added[0], func[select].first_result.expect("a select has a result"));
assert_eq!(carries(&func, 0), vec![func[add].first_result.expect("an add has a result")]);
}
#[test]
fn arms_that_factor_away_entirely_are_not_too_long() {
let steps = [Opcode::Add, Opcode::Sub, Opcode::Mul];
let mut func = same_operation(&steps);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Missed, super::ARMS_TOO_LONG), 0);
assert_eq!(stats.count(Kind::Optimized, super::FACTORED), 3);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
let written = opcodes(&func, 0);
assert_eq!(written.iter().filter(|&&op| op == Opcode::Select).count(), 3);
for step in steps {
assert_eq!(written.iter().filter(|&&op| op == step).count(), 1, "{step:?} once");
}
}
#[test]
fn arms_that_agree_in_every_operand_need_no_select() {
let mut names = Interner::new();
let int = Type::int(32);
let signature = Signature::new().with_params(&[int, int, int]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let left = func.append_param(head, int);
let right = func.append_param(head, int);
let operand = func.append_param(head, int);
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, int);
let mut build = Builder::new(&mut func, head);
let shared = build.iconst(int, 3);
let test = build.icmp(IntPred::Slt, left, right);
build.br_if(test, arms[0], &[], arms[1], &[]);
for &arm in &arms {
let mut build = Builder::new(&mut func, arm);
let it = build.binary(Opcode::Add, operand, shared, Flags::default());
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FACTORED), 1);
assert_eq!(
opcodes(&func, 0),
vec![Opcode::IConst, Opcode::ICmp, Opcode::Add, Opcode::Jump],
"one add and nothing to choose between"
);
}
#[test]
fn arms_that_do_different_things_are_not_factored() {
let mut names = Interner::new();
let int = Type::int(32);
let signature = Signature::new().with_params(&[int, int, int, int]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let left = func.append_param(head, int);
let right = func.append_param(head, int);
let operands = [func.append_param(head, int), func.append_param(head, int)];
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, int);
let mut build = Builder::new(&mut func, head);
let shared = build.iconst(int, 3);
let test = build.icmp(IntPred::Slt, left, right);
build.br_if(test, arms[0], &[], arms[1], &[]);
for ((&arm, operand), opcode) in arms.iter().zip(operands).zip([Opcode::Add, Opcode::Sub]) {
let mut build = Builder::new(&mut func, arm);
let it = build.binary(opcode, operand, shared, Flags::default());
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FACTORED), 0);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
assert_eq!(
opcodes(&func, 0),
vec![
Opcode::IConst,
Opcode::ICmp,
Opcode::Add,
Opcode::Sub,
Opcode::Select,
Opcode::Jump
],
"both operations hoisted and a select between their answers"
);
}
#[test]
fn arms_that_differ_in_two_operands_are_not_factored() {
let mut names = Interner::new();
let int = Type::int(32);
let signature = Signature::new().with_params(&[int, int, int, int, int, int]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let left = func.append_param(head, int);
let right = func.append_param(head, int);
let first = [func.append_param(head, int), func.append_param(head, int)];
let second = [func.append_param(head, int), func.append_param(head, int)];
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, int);
let mut build = Builder::new(&mut func, head);
let test = build.icmp(IntPred::Slt, left, right);
build.br_if(test, arms[0], &[], arms[1], &[]);
for ((&arm, one), two) in arms.iter().zip(first).zip(second) {
let mut build = Builder::new(&mut func, arm);
let it = build.binary(Opcode::Add, one, two, Flags::default());
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FACTORED), 0);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
assert_eq!(opcodes(&func, 0).iter().filter(|&&op| op == Opcode::Add).count(), 2);
}
#[test]
fn an_operation_read_more_than_once_is_not_factored() {
let mut names = Interner::new();
let int = Type::int(32);
let signature = Signature::new().with_params(&[int, int, int, int]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let left = func.append_param(head, int);
let right = func.append_param(head, int);
let operands = [func.append_param(head, int), func.append_param(head, int)];
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let params = [func.append_param(join, int), func.append_param(join, int)];
let mut build = Builder::new(&mut func, head);
let shared = build.iconst(int, 3);
let test = build.icmp(IntPred::Slt, left, right);
build.br_if(test, arms[0], &[], arms[1], &[]);
for (&arm, operand) in arms.iter().zip(operands) {
let mut build = Builder::new(&mut func, arm);
let it = build.binary(Opcode::Add, operand, shared, Flags::default());
build.jump(join, &[it, it]);
}
let mut build = Builder::new(&mut func, join);
build.ret(¶ms);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FACTORED), 0);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
assert_eq!(opcodes(&func, 0).iter().filter(|&&op| op == Opcode::Add).count(), 2);
}
#[test]
fn a_triangle_factors_nothing() {
let mut names = Interner::new();
let int = Type::int(32);
let signature = Signature::new().with_params(&[int, int, int]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let left = func.append_param(head, int);
let right = func.append_param(head, int);
let operand = func.append_param(head, int);
let arm = func.create_block();
let join = func.create_block();
let param = func.append_param(join, int);
let mut build = Builder::new(&mut func, head);
let shared = build.iconst(int, 3);
let test = build.icmp(IntPred::Slt, left, right);
build.br_if(test, arm, &[], join, &[operand]);
let mut build = Builder::new(&mut func, arm);
let it = build.binary(Opcode::Add, operand, shared, Flags::default());
build.jump(join, &[it]);
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let stats = phiopt(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FACTORED), 0);
assert_eq!(stats.count(Kind::Optimized, super::CONVERTED), 1);
}
}