use std::collections::HashMap;
use rucc_cost::heuristics::PREDICT_EXPECT;
use rucc_ir::{Block, Def, Extra, Func, Hint, Inst, IntPred, Opcode, Value};
use crate::fold::constant;
use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats, uses};
const PLACED: &str = "branch weight written from a __builtin_expect on its condition";
const NO_BRANCH: &str = "__builtin_expect dropped, no branch in this function is on its value";
const NO_FUEL: &str = "__builtin_expect kept, the pass ran out of fuel";
#[derive(Debug)]
pub struct Expect;
impl Pass for Expect {
fn name(&self) -> &'static str {
"expect"
}
fn describe(&self) -> &'static str {
"what __builtin_expect said moves onto the arms of the branch it was said about"
}
fn preserves(&self) -> Preserved {
Preserved::ALL.without(Analysis::Liveness).without(Analysis::Frequencies)
}
fn required(&self) -> bool {
true
}
fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
let mut stats = Stats::new();
let mut hints: Vec<Inst> = Vec::new();
for block in func.blocks().collect::<Vec<Block>>() {
for inst in func.insts(block) {
if func[inst].opcode == Opcode::Expect {
hints.push(inst);
}
}
}
if hints.is_empty() {
return stats;
}
let mut placed = 0;
for block in func.blocks().collect::<Vec<Block>>() {
let Some(term) = func.terminator(block) else { continue };
if func[term].opcode != Opcode::BrIf {
continue;
}
let Some(&cond) = func[func[term].args].first() else { continue };
let Some((inst, sense)) = through(func, cond) else { continue };
let Some(parts) = claim(func, inst, sense) else { continue };
if !fuel.take() {
stats.missed(NO_FUEL);
break;
}
write(func, term, parts);
stats.optimized(PLACED);
placed += 1;
}
let mut forward: HashMap<Value, Value> = HashMap::new();
for &inst in &hints {
let args = &func[func[inst].args];
let (Some(&result), Some(&value)) = (func[inst].first_result.as_ref(), args.first())
else {
continue;
};
forward.insert(result, value);
}
uses::substitute(func, &forward);
for &inst in &hints {
func.remove_inst(inst);
}
for _ in placed..hints.len() {
stats.note(NO_BRANCH);
}
stats
}
}
fn through(func: &Func, cond: Value) -> Option<(Inst, bool)> {
let mut value = cond;
let mut sense = true;
loop {
let Def::Result { inst, .. } = func[value].def else { return None };
let data = &func[inst];
match data.opcode {
Opcode::Expect => return Some((inst, sense)),
Opcode::ZExt | Opcode::SExt => value = *func[data.args].first()?,
Opcode::ICmp => {
let Extra::IntPred(pred) = data.extra else { return None };
let args = &func[data.args];
let lhs = *args.first()?;
let rhs = *args.get(1)?;
if literal(func, rhs)? != 0 {
return None;
}
match pred {
IntPred::Ne => {}
IntPred::Eq => sense = !sense,
_ => return None,
}
value = lhs;
}
_ => return None,
}
}
}
fn claim(func: &Func, inst: Inst, sense: bool) -> Option<u32> {
let args = &func[func[inst].args];
let value = literal(func, *args.get(1)?)?;
let parts = match args.get(2) {
Some(&given) => u32::try_from(literal(func, given)?).ok()?.min(Hint::SCALE),
None => PREDICT_EXPECT * Hint::SCALE / 100,
};
let met = (value != 0) == sense;
Some(if met { parts } else { Hint::SCALE - parts })
}
fn literal(func: &Func, value: Value) -> Option<i128> {
let mut value = value;
loop {
if let Some((bits, ty)) = constant(func, value) {
return Some(bits.signed(ty));
}
let Def::Result { inst, .. } = func[value].def else { return None };
let data = &func[inst];
match data.opcode {
Opcode::ZExt | Opcode::SExt => value = *func[data.args].first()?,
_ => return None,
}
}
}
fn write(func: &mut Func, term: Inst, parts: u32) {
let hint = Hint::parts(parts);
for (at, hint) in func.target_list(term).iter().zip([hint, hint.complement()]) {
let call = func[at];
func.set_block_call(at, rucc_ir::BlockCall { hint, ..call });
}
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Builder, InstData, Signature, Type};
use super::*;
fn blank(blocks: usize) -> (Interner, Func, Vec<Block>) {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let list = (0..blocks).map(|_| func.create_block()).collect();
(names, func, list)
}
fn shaped(hint: i128, parts: Option<i128>) -> (Interner, Func, Vec<Block>) {
let (names, mut func, at) = blank(3);
let i64_ = Type::int(64);
let value = func.append_param(at[0], i64_);
let mut build = Builder::new(&mut func, at[0]);
let hint = build.iconst(i64_, hint);
let mut operands = vec![value, hint];
if let Some(parts) = parts {
let parts = build.iconst(i64_, parts);
operands.push(parts);
}
let args = build.func().push_values(&operands);
let wrapped = build.value(InstData { args, ..InstData::new(Opcode::Expect) }, i64_);
let zero = build.iconst(i64_, 0);
let cond = build.icmp(IntPred::Ne, wrapped, zero);
build.br_if(cond, at[1], &[], at[2], &[]);
for block in [at[1], at[2]] {
let mut build = Builder::new(&mut func, block);
let answer = build.iconst(Type::int(32), 0);
build.ret(&[answer]);
}
(names, func, at)
}
fn arms(func: &Func, block: Block) -> Vec<Option<u32>> {
let term = func.terminator(block).expect("a branch");
func.target_list(term).iter().map(|at| func[at].hint.taken()).collect()
}
fn run(func: &mut Func) -> Stats {
Expect.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
}
#[test]
fn a_hint_of_one_names_the_arm_taken_when_the_condition_holds() {
let (_, mut func, at) = shaped(1, None);
run(&mut func);
assert_eq!(arms(&func, at[0]), [Some(9_000), Some(1_000)]);
}
#[test]
fn a_hint_of_zero_names_the_other_arm() {
let (_, mut func, at) = shaped(0, None);
run(&mut func);
assert_eq!(arms(&func, at[0]), [Some(1_000), Some(9_000)]);
}
#[test]
fn a_hint_behind_the_conversion_the_prototype_asked_for_is_still_a_hint() {
let (_, mut func, at) = blank(3);
let i64_ = Type::int(64);
let value = func.append_param(at[0], i64_);
let mut build = Builder::new(&mut func, at[0]);
let narrow = build.iconst(Type::int(32), 0);
let hint = build.unary(Opcode::SExt, narrow, i64_);
let args = build.func().push_values(&[value, hint]);
let wrapped = build.value(InstData { args, ..InstData::new(Opcode::Expect) }, i64_);
let zero = build.iconst(i64_, 0);
let cond = build.icmp(IntPred::Ne, wrapped, zero);
build.br_if(cond, at[1], &[], at[2], &[]);
for block in [at[1], at[2]] {
let mut build = Builder::new(&mut func, block);
let answer = build.iconst(Type::int(32), 0);
build.ret(&[answer]);
}
run(&mut func);
assert_eq!(arms(&func, at[0]), [Some(1_000), Some(9_000)]);
}
#[test]
fn a_probability_the_program_wrote_is_the_one_the_branch_gets() {
let (_, mut func, at) = shaped(1, Some(7_500));
run(&mut func);
assert_eq!(arms(&func, at[0]), [Some(7_500), Some(2_500)]);
}
#[test]
fn a_probability_with_a_hint_of_zero_is_about_the_other_arm() {
let (_, mut func, at) = shaped(0, Some(7_500));
run(&mut func);
assert_eq!(arms(&func, at[0]), [Some(2_500), Some(7_500)]);
}
#[test]
fn the_instruction_goes_and_its_readers_read_what_it_was_given() {
let (_, mut func, at) = shaped(1, None);
run(&mut func);
let left: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block)).filter(is_expect(&func)).collect();
assert!(left.is_empty(), "the wrapper is gone");
let term = func.terminator(at[0]).expect("a branch");
let cond = *func[func[term].args].first().expect("a condition");
let Def::Result { inst, .. } = func[cond].def else { panic!("a comparison") };
let read = *func[func[inst].args].first().expect("a left hand side");
assert!(matches!(func[read].def, Def::Param { .. }), "it reads the parameter");
}
fn is_expect(func: &Func) -> impl Fn(&Inst) -> bool + use<'_> {
move |&inst| func[inst].opcode == Opcode::Expect
}
#[test]
fn a_function_with_no_hint_in_it_is_left_alone() {
let (_, mut func, at) = blank(3);
let i64_ = Type::int(64);
let value = func.append_param(at[0], i64_);
let mut build = Builder::new(&mut func, at[0]);
let zero = build.iconst(i64_, 0);
let cond = build.icmp(IntPred::Ne, value, zero);
build.br_if(cond, at[1], &[], at[2], &[]);
for block in [at[1], at[2]] {
let mut build = Builder::new(&mut func, block);
let answer = build.iconst(Type::int(32), 0);
build.ret(&[answer]);
}
let stats = run(&mut func);
assert!(!stats.changed(), "nothing to do");
assert_eq!(arms(&func, at[0]), [None, None]);
}
#[test]
fn a_condition_that_is_a_comparison_against_zero_the_other_way_flips_the_arms() {
let (_, mut func, at) = blank(3);
let i64_ = Type::int(64);
let value = func.append_param(at[0], i64_);
let mut build = Builder::new(&mut func, at[0]);
let hint = build.iconst(i64_, 1);
let args = build.func().push_values(&[value, hint]);
let wrapped = build.value(InstData { args, ..InstData::new(Opcode::Expect) }, i64_);
let zero = build.iconst(i64_, 0);
let cond = build.icmp(IntPred::Eq, wrapped, zero);
build.br_if(cond, at[1], &[], at[2], &[]);
for block in [at[1], at[2]] {
let mut build = Builder::new(&mut func, block);
let answer = build.iconst(Type::int(32), 0);
build.ret(&[answer]);
}
run(&mut func);
assert_eq!(arms(&func, at[0]), [Some(1_000), Some(9_000)]);
}
#[test]
fn a_hint_on_a_value_no_branch_reads_leaves_nothing_behind() {
let (_, mut func, at) = blank(1);
let i64_ = Type::int(64);
let value = func.append_param(at[0], i64_);
let mut build = Builder::new(&mut func, at[0]);
let hint = build.iconst(i64_, 1);
let args = build.func().push_values(&[value, hint]);
let wrapped = build.value(InstData { args, ..InstData::new(Opcode::Expect) }, i64_);
build.ret(&[wrapped]);
run(&mut func);
let left: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block)).filter(is_expect(&func)).collect();
assert!(left.is_empty(), "the wrapper is gone");
let term = func.terminator(at[0]).expect("a return");
let answer = *func[func[term].args].first().expect("a returned value");
assert!(matches!(func[answer].def, Def::Param { .. }), "it returns the parameter");
}
}