use rucc_cost::heuristics;
use rucc_ir::{Block, Builder, Func, Inst, 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 ARM_HAS_EFFECTS: &str =
"branch kept, an arm does something that only happens on the path it is on";
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 };
if let Some(reason) = refused(func, &shape) {
stats.missed(reason);
continue;
}
let work = shape.arms.map(|arm| arm.map_or(0, |block| length(func, block)));
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);
an.clear();
stats.optimized(CONVERTED);
}
stats
}
}
struct Diamond {
head: Block,
cond: Value,
join: Block,
arms: [Option<Block>; 2],
args: [Vec<Value>; 2],
}
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) -> 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);
}
for &arm in shape.arms.iter().flatten() {
for inst in func.insts(arm) {
if func.is_terminator(inst) {
continue;
}
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
}
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
}
fn selectable(ty: Type) -> bool {
ty.is_scalar() && ty.is_int() && matches!(ty.bits(), 8 | 16 | 32 | 64)
}
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)
}
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) {
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);
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);
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 (&then, &other) in shape.args[0].iter().zip(&shape.args[1]) {
let same = agree(build.func(), then, other);
args.push(if same { then } else { build.select(shape.cond, then, other) });
}
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 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);
let info = MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
};
build.store(what, address, info, Flags::NONE);
}
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 an_arm_that_does_something_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::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]);
}
}