use rucc_cost::heuristics;
use rucc_ir::{Block, Builder, Flags, Func, Imm, Opcode, Type, Value};
use crate::fold::constant;
use crate::phiopt::{Diamond, diamond, length, speculatable, unpredictable};
use crate::simplify_cfg::{self, Bindings};
use crate::{Analyses, Fuel, Pass, Preserved, Stats};
const COLLAPSED: &str =
"both halves of an and-and or an or-or worked out at once, and the branch went";
const RIGHT_HAS_EFFECTS: &str =
"branch kept, working out the right operand touches memory or calls something";
const RIGHT_MAY_TRAP: &str =
"branch kept, the right operand divides and working it out early could trap";
const TOO_MUCH_WORK: &str = "branch kept, the right operand is more work than one branch is worth";
const BRANCH_IS_PREDICTED: &str = "branch kept, the estimate says it goes one way nearly always";
const CONDITION_IS_DECIDED: &str = "branch kept, the left operand is already decided";
const NO_FUEL: &str = "branch kept, the pass ran out of fuel";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShortCircuit;
impl Pass for ShortCircuit {
fn name(&self) -> &'static str {
"short-circuit"
}
fn describe(&self) -> &'static str {
"an and-and or an or-or works both halves out at once when the right half is safe to"
}
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 Some(plan) = collapsed(func, &shape) else { continue };
if let Some(reason) = refused(func, &shape) {
stats.missed(reason);
continue;
}
let work: u32 = shape.arms.iter().flatten().map(|&arm| length(func, arm)).sum();
if work > 0 {
if work > heuristics::SHORT_CIRCUIT_INSTRUCTIONS {
stats.missed(TOO_MUCH_WORK);
continue;
}
if !unpredictable(an.frequencies(func).taken(head, 0)) {
stats.missed(BRANCH_IS_PREDICTED);
continue;
}
}
if !fuel.take() {
stats.missed(NO_FUEL);
break;
}
fold(func, &shape, &plan);
an.clear();
stats.optimized(COLLAPSED);
}
stats
}
}
struct Collapse {
right: Value,
joined: Opcode,
}
fn collapsed(func: &Func, shape: &Diamond) -> Option<Collapse> {
let [param] = func[shape.join].params[..] else { return None };
if func[param].ty != Type::I1 {
return None;
}
let sides = [shape.args[0][0], shape.args[1][0]];
let known = [constant(func, sides[0]), constant(func, sides[1])];
let settled = |imm: Imm| imm.unsigned() != 0;
match known {
[None, Some((imm, _))] if !settled(imm) => {
Some(Collapse { right: sides[0], joined: Opcode::And })
}
[Some((imm, _)), None] if settled(imm) => {
Some(Collapse { right: sides[1], joined: Opcode::Or })
}
_ => None,
}
}
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(RIGHT_HAS_EFFECTS);
}
if !speculatable(func, inst) {
return Some(RIGHT_MAY_TRAP);
}
}
}
None
}
fn fold(func: &mut Func, shape: &Diamond, plan: &Collapse) {
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<_>>() {
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 cond = build.binary(plan.joined, shape.cond, plan.right, Flags::NONE);
build.jump(shape.join, &[cond]);
for &arm in shape.arms.iter().flatten() {
func.remove_block(arm);
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use rucc_base::Interner;
use rucc_ir::{
Block, BlockCall, Builder, Extra, Flags, Func, IntPred, MemInfo, MemOrder, Opcode,
Restrict, Signature, Type, Value,
};
use super::ShortCircuit;
use crate::stats::Kind;
use crate::{Analyses, Fuel, Pass, Stats};
fn collapse(func: &mut Func) -> Stats {
ShortCircuit.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 ends_at(func: &Func, inputs: &[i128]) -> usize {
let mut values: HashMap<Value, i128> = HashMap::new();
let mut block = func.entry().expect("a function with blocks in it");
for (¶m, &input) in func[block].params.iter().zip(inputs) {
values.insert(param, input);
}
loop {
let mut end = None;
for inst in func.insts(block) {
if func.is_terminator(inst) {
end = Some(inst);
break;
}
let data = func[inst];
let args: Vec<i128> = func[data.args].iter().map(|arg| values[arg]).collect();
let result = func[inst].first_result.expect("one result");
let it = match data.opcode {
Opcode::IConst => {
let (imm, ty) =
crate::fold::constant(func, result).expect("a constant is one");
imm.signed(ty)
}
Opcode::ICmp => {
let Extra::IntPred(pred) = data.extra else {
panic!("a comparison carries its predicate");
};
i128::from(match pred {
IntPred::Slt => args[0] < args[1],
IntPred::Sgt => args[0] > args[1],
other => panic!("nothing here compares with {other:?}"),
})
}
Opcode::And => i128::from(args[0] != 0 && args[1] != 0),
Opcode::Or => i128::from(args[0] != 0 || args[1] != 0),
Opcode::Add => args[0] + args[1],
other => panic!("nothing here writes a {other:?}"),
};
values.insert(result, it);
}
let end = end.expect("every block here ends in a terminator");
let data = func[end];
let call = match data.opcode {
Opcode::Jump => func.successors(end).next().expect("a jump has one edge"),
Opcode::BrIf => {
let cond = values[&func[data.args][0]];
let mut edges = func.successors(end);
let then = edges.next().expect("a branch has two edges");
let other = edges.next().expect("a branch has two edges");
if cond == 0 { other } else { then }
}
Opcode::Return => return block.index(),
other => panic!("nothing here ends a block with a {other:?}"),
};
let carried: Vec<i128> = func[call.args].iter().map(|arg| values[arg]).collect();
for (¶m, arg) in func[call.block].params.iter().zip(carried) {
values.insert(param, arg);
}
block = call.block;
}
}
fn short_circuit(settled: bool) -> 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), func.append_param(head, int)];
let right = [func.append_param(head, int), func.append_param(head, int)];
let arm = func.create_block();
let join = func.create_block();
let bit = func.append_param(join, Type::I1);
let ends = [func.create_block(), func.create_block()];
let mut build = Builder::new(&mut func, head);
let test = build.icmp(IntPred::Slt, left[0], left[1]);
let already = build.iconst(Type::I1, i128::from(settled));
if settled {
build.br_if(test, join, &[already], arm, &[]);
} else {
build.br_if(test, arm, &[], join, &[already]);
}
let mut build = Builder::new(&mut func, arm);
let test = build.icmp(IntPred::Slt, right[0], right[1]);
build.jump(join, &[test]);
let mut build = Builder::new(&mut func, join);
build.br_if(bit, ends[0], &[], ends[1], &[]);
for block in ends {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
func
}
fn both_ways() -> Vec<Vec<i128>> {
let numbers = |holds: bool| if holds { [0, 1] } else { [1, 0] };
let mut out = Vec::new();
for left in [false, true] {
for right in [false, true] {
let mut inputs = numbers(left).to_vec();
inputs.extend(numbers(right));
out.push(inputs);
}
}
out
}
fn into_the_arm(func: &mut Func, write: impl FnOnce(&mut Builder<'_>)) {
let arm = Block::from_usize(1);
let term = func.terminator(arm).expect("the jump to the join");
func.remove_inst(term);
let mut build = Builder::new(func, arm);
write(&mut build);
func.append_inst(arm, term);
}
#[test]
fn an_and_and_stops_being_a_branch_and_becomes_an_and() {
let mut func = short_circuit(false);
let stats = collapse(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COLLAPSED), 1);
assert_eq!(
opcodes(&func, 0),
vec![Opcode::ICmp, Opcode::IConst, Opcode::ICmp, Opcode::And, Opcode::Jump]
);
assert_eq!(goes_to(&func, 0), vec![2]);
assert_eq!(blocks(&func), vec![0, 2, 3, 4]);
}
#[test]
fn an_or_or_becomes_an_or() {
let mut func = short_circuit(true);
let stats = collapse(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COLLAPSED), 1);
assert_eq!(
opcodes(&func, 0),
vec![Opcode::ICmp, Opcode::IConst, Opcode::ICmp, Opcode::Or, Opcode::Jump]
);
assert_eq!(goes_to(&func, 0), vec![2]);
assert_eq!(blocks(&func), vec![0, 2, 3, 4]);
}
#[test]
fn every_way_the_two_operands_can_go_ends_where_it_did() {
for settled in [false, true] {
let before = short_circuit(settled);
let mut after = short_circuit(settled);
collapse(&mut after);
for inputs in both_ways() {
assert_eq!(
ends_at(&before, &inputs),
ends_at(&after, &inputs),
"the two operands as {inputs:?}, short circuiting on {settled}"
);
}
}
}
#[test]
fn a_right_operand_worked_out_above_the_branch_is_folded() {
let mut names = Interner::new();
let int = Type::int(32);
let signature = Signature::new().with_params(&[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 arm = func.create_block();
let join = func.create_block();
let bit = func.append_param(join, Type::I1);
let ends = [func.create_block(), func.create_block()];
let mut build = Builder::new(&mut func, head);
let first = build.icmp(IntPred::Slt, left, right);
let second = build.icmp(IntPred::Sgt, left, right);
let already = build.iconst(Type::I1, 0);
build.br_if(first, arm, &[], join, &[already]);
let mut build = Builder::new(&mut func, arm);
build.jump(join, &[second]);
let mut build = Builder::new(&mut func, join);
build.br_if(bit, ends[0], &[], ends[1], &[]);
for block in ends {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
let stats = collapse(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COLLAPSED), 1);
assert_eq!(stats.count(Kind::Missed, super::BRANCH_IS_PREDICTED), 0);
assert_eq!(blocks(&func), vec![0, 2, 3, 4]);
}
#[test]
fn a_load_on_the_right_of_an_and_and_keeps_its_branch() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR]);
let mut func = Func::new(names.intern("f"), signature);
let head = func.create_block();
let pointer = func.append_param(head, Type::PTR);
let arm = func.create_block();
let join = func.create_block();
let bit = func.append_param(join, Type::I1);
let ends = [func.create_block(), func.create_block()];
let mut build = Builder::new(&mut func, head);
let null = build.iconst(Type::int(64), 0);
let null = build.unary(Opcode::IntToPtr, null, Type::PTR);
let first = build.icmp(IntPred::Sgt, pointer, null);
let already = build.iconst(Type::I1, 0);
build.br_if(first, arm, &[], join, &[already]);
let mut build = Builder::new(&mut func, arm);
let info = MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
};
let field = build.load(Type::int(32), pointer, info, Flags::NONE);
let zero = build.iconst(Type::int(32), 0);
let second = build.icmp(IntPred::Sgt, field, zero);
build.jump(join, &[second]);
let mut build = Builder::new(&mut func, join);
build.br_if(bit, ends[0], &[], ends[1], &[]);
for block in ends {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
let stats = collapse(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COLLAPSED), 0);
assert_eq!(stats.count(Kind::Missed, super::RIGHT_HAS_EFFECTS), 1);
assert_eq!(blocks(&func), vec![0, 1, 2, 3, 4]);
}
#[test]
fn a_right_operand_that_stores_something_keeps_its_branch() {
let mut func = short_circuit(false);
into_the_arm(&mut func, |build| {
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);
});
let stats = collapse(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COLLAPSED), 0);
assert_eq!(stats.count(Kind::Missed, super::RIGHT_HAS_EFFECTS), 1);
}
#[test]
fn a_right_operand_that_divides_by_something_unknown_keeps_its_branch() {
let mut func = short_circuit(false);
let operands: Vec<Value> = func[Block::from_usize(0)].params.to_vec();
into_the_arm(&mut func, |build| {
build.binary(Opcode::SDiv, operands[2], operands[3], Flags::NONE);
});
let stats = collapse(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COLLAPSED), 0);
assert_eq!(stats.count(Kind::Missed, super::RIGHT_MAY_TRAP), 1);
}
#[test]
fn a_right_operand_with_more_work_in_it_than_the_budget_keeps_its_branch() {
let mut func = short_circuit(false);
into_the_arm(&mut func, |build| {
let mut it = build.iconst(Type::int(32), 1);
for _ in 0..2 {
it = build.binary(Opcode::Add, it, it, Flags::NONE);
}
});
let stats = collapse(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COLLAPSED), 0);
assert_eq!(stats.count(Kind::Missed, super::TOO_MUCH_WORK), 1);
assert_eq!(blocks(&func), vec![0, 1, 2, 3, 4]);
}
#[test]
fn a_branch_that_is_already_decided_is_left_for_simplify_cfg() {
let mut func = short_circuit(false);
let head = Block::from_usize(0);
let already = func.insts(head).nth(1).expect("the bit the branch carries");
let already = func[already].first_result.expect("a constant is one value");
let term = func.terminator(head).expect("the branch");
func.remove_inst(term);
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 decided = build.icmp(IntPred::Sgt, one, zero);
build.br_if(decided, Block::from_usize(1), &[], Block::from_usize(2), &[already]);
let stats = collapse(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COLLAPSED), 0);
assert_eq!(stats.count(Kind::Missed, super::CONDITION_IS_DECIDED), 1);
assert_eq!(blocks(&func), vec![0, 1, 2, 3, 4]);
}
#[test]
fn a_bit_that_is_known_the_wrong_way_round_is_left_alone() {
let mut func = short_circuit(false);
let head = Block::from_usize(0);
let term = func.terminator(head).expect("the branch");
let cond = func[func[term].args][0];
let edges: Vec<BlockCall> = func.successors(term).collect();
func.remove_inst(term);
let mut build = Builder::new(&mut func, head);
let flipped = build.iconst(Type::I1, 1);
build.br_if(cond, edges[0].block, &[], edges[1].block, &[flipped]);
let stats = collapse(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COLLAPSED), 0);
assert_eq!(blocks(&func), vec![0, 1, 2, 3, 4]);
}
#[test]
fn a_join_carrying_more_than_the_one_bit_is_left_to_phiopt() {
let mut func = short_circuit(false);
let join = Block::from_usize(2);
func.append_param(join, Type::int(32));
for block in [Block::from_usize(0), Block::from_usize(1)] {
let term = func.terminator(block).expect("every block here has one");
let edges: Vec<BlockCall> = func.successors(term).collect();
func.remove_inst(term);
let extra = Builder::new(&mut func, block).iconst(Type::int(32), 5);
let mut calls = Vec::new();
for edge in &edges {
let mut args = func[edge.args].to_vec();
if edge.block == join {
args.push(extra);
}
let args = func.push_values(&args);
calls.push(BlockCall { args, ..*edge });
}
let calls = func.push_block_calls(&calls);
func[term].extra = Extra::Targets(calls);
func.append_inst(block, term);
}
let stats = collapse(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COLLAPSED), 0);
assert_eq!(blocks(&func), vec![0, 1, 2, 3, 4]);
}
#[test]
fn fuel_stops_the_fold_where_it_stands() {
let mut func = short_circuit(false);
let mut fuel = Fuel::of(0);
let stats = ShortCircuit.run(&mut func, &mut Analyses::new(), &mut fuel);
assert_eq!(stats.count(Kind::Optimized, super::COLLAPSED), 0);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
assert_eq!(goes_to(&func, 0), vec![1, 2]);
}
}