use std::collections::HashMap;
use rucc_base::Symbol;
use rucc_cost::heuristics::{
PREDICT_CALL_NOT_TAKEN, PREDICT_COLD_CALL, PREDICT_CONTINUE_TAKEN, PREDICT_EXPECT,
PREDICT_LOOP_EXIT_NOT_TAKEN, PREDICT_LOOP_GUARD_TAKEN, PREDICT_NEGATIVE_RETURN,
PREDICT_NEVER_RETURNS, PREDICT_NULL_RETURN, PREDICT_POINTER_NOT_NULL, PREDICT_RETURN_BLOCKS,
};
use rucc_ir::{AttrSet, Attrs, Block, Def, Extra, Func, Inst, IntPred, Module, Opcode, Value};
use crate::cfg::Cfg;
use crate::fold::constant;
use crate::loops::Loops;
use crate::profile::{Probability, Quality};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Predictor {
Expect,
NeverReturns,
ColdCall,
LoopExit,
LoopGuard,
PointerNotNull,
NegativeReturn,
NullReturn,
CallNotTaken,
Continue,
Nothing,
}
impl Predictor {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Expect => "__builtin_expect",
Self::NeverReturns => "the arm that does not come back",
Self::ColdCall => "the arm that calls a cold function",
Self::LoopExit => "the loop exit",
Self::LoopGuard => "the loop guard",
Self::PointerNotNull => "the pointer is not null",
Self::NegativeReturn => "the arm that returns a negative number",
Self::NullReturn => "the arm that returns null",
Self::CallNotTaken => "the arm that calls something",
Self::Continue => "the continue",
Self::Nothing => "nothing, so even",
}
}
#[must_use]
pub const fn hit_rate(self) -> u32 {
match self {
Self::Expect => PREDICT_EXPECT,
Self::NeverReturns => PREDICT_NEVER_RETURNS,
Self::ColdCall => PREDICT_COLD_CALL,
Self::LoopExit => PREDICT_LOOP_EXIT_NOT_TAKEN,
Self::LoopGuard => PREDICT_LOOP_GUARD_TAKEN,
Self::PointerNotNull => PREDICT_POINTER_NOT_NULL,
Self::NegativeReturn => PREDICT_NEGATIVE_RETURN,
Self::NullReturn => PREDICT_NULL_RETURN,
Self::CallNotTaken => PREDICT_CALL_NOT_TAKEN,
Self::Continue => PREDICT_CONTINUE_TAKEN,
Self::Nothing => 50,
}
}
pub const ORDER: [Self; 10] = [
Self::Expect,
Self::NeverReturns,
Self::ColdCall,
Self::LoopExit,
Self::LoopGuard,
Self::PointerNotNull,
Self::NegativeReturn,
Self::NullReturn,
Self::CallNotTaken,
Self::Continue,
];
}
impl std::fmt::Display for Predictor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Default)]
pub struct Callees {
known: HashMap<Symbol, AttrSet>,
}
impl Callees {
#[must_use]
pub fn nothing() -> Self {
Self::default()
}
#[must_use]
pub fn of_module(module: &Module) -> Self {
let mut known = HashMap::new();
for id in module.funcs() {
let func = &module[id];
known.insert(func.name, func.attrs.set);
}
Self { known }
}
pub fn record(&mut self, name: Symbol, attrs: Attrs) {
self.known.insert(name, attrs.set);
}
#[must_use]
pub fn never_returns(&self, name: Symbol) -> bool {
self.known.get(&name).is_some_and(|set| set.contains(AttrSet::NORETURN))
}
#[must_use]
pub fn is_cold(&self, name: Symbol) -> bool {
self.known.get(&name).is_some_and(|set| set.contains(AttrSet::COLD))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Predictions {
edges: Vec<Vec<Probability>>,
by: Vec<Predictor>,
}
impl Predictions {
#[must_use]
pub fn of(func: &Func, cfg: &Cfg, loops: &Loops, callees: &Callees) -> Self {
let width = cfg.capacity();
let mut edges: Vec<Vec<Probability>> = vec![Vec::new(); width];
let mut by = vec![Predictor::Nothing; width];
let returns = returning(func, cfg);
for block in func.blocks() {
let Some(term) = func.terminator(block) else { continue };
let succs = cfg.successors(block);
if succs.len() == 2 && func[term].opcode == Opcode::BrIf {
let (taken, who) = branch(func, cfg, loops, callees, &returns, block);
edges[block.index()] = vec![taken, taken.complement()];
by[block.index()] = who;
continue;
}
let (parts, who) = share(func, cfg, callees, &returns, block, term);
edges[block.index()] = parts;
by[block.index()] = who;
}
Self { edges, by }
}
#[must_use]
pub fn edges(&self, block: Block) -> &[Probability] {
self.edges.get(block.index()).map_or(&[], Vec::as_slice)
}
#[must_use]
pub fn taken(&self, block: Block, index: usize) -> Probability {
self.edges(block).get(index).copied().unwrap_or_else(Probability::never)
}
#[must_use]
pub fn by(&self, block: Block) -> Predictor {
self.by.get(block.index()).copied().unwrap_or(Predictor::Nothing)
}
}
fn toward(first: bool, percent: u32) -> Probability {
let likely = Probability::percent(percent, Quality::Guessed);
if first { likely } else { likely.complement() }
}
fn branch(
func: &Func,
cfg: &Cfg,
loops: &Loops,
callees: &Callees,
returns: &[bool],
block: Block,
) -> (Probability, Predictor) {
let succs = cfg.successors(block);
let (first, second) = (succs[0], succs[1]);
let term = func.terminator(block).expect("a block with successors has a terminator");
let cond = *func[func[term].args].first().expect("a br_if has a condition");
if let Some(taken) = expect(func, cond) {
return (taken, Predictor::Expect);
}
let gone = |at: Block| never_comes_back(func, callees, returns, at);
if gone(first) != gone(second) {
return (toward(!gone(first), PREDICT_NEVER_RETURNS), Predictor::NeverReturns);
}
let cold = |at: Block| calls_named(func, at, |name| callees.is_cold(name));
if cold(first) != cold(second) {
return (toward(!cold(first), PREDICT_COLD_CALL), Predictor::ColdCall);
}
let leaves = |at: Block| match loops.innermost(block) {
Some(id) => !loops.contains(id, at),
None => false,
};
if leaves(first) != leaves(second) {
return (toward(!leaves(first), PREDICT_LOOP_EXIT_NOT_TAKEN), Predictor::LoopExit);
}
let enters = |at: Block| enters_loop(cfg, loops, block, at);
if enters(first) != enters(second) {
return (toward(enters(first), PREDICT_LOOP_GUARD_TAKEN), Predictor::LoopGuard);
}
if let Some(taken) = pointer_null(func, cond) {
return (taken, Predictor::PointerNotNull);
}
let gives = |at: Block| returns_constant(func, cfg, at);
let negative = |at: Block| matches!(gives(at), Some(Returned::Negative));
if negative(first) != negative(second) {
return (toward(!negative(first), PREDICT_NEGATIVE_RETURN), Predictor::NegativeReturn);
}
let null = |at: Block| matches!(gives(at), Some(Returned::Null));
if null(first) != null(second) {
return (toward(!null(first), PREDICT_NULL_RETURN), Predictor::NullReturn);
}
let calls = |at: Block| has_call(func, at);
if calls(first) != calls(second) {
return (toward(!calls(first), PREDICT_CALL_NOT_TAKEN), Predictor::CallNotTaken);
}
let again = |at: Block| goes_round_again(loops, block, at);
if again(first) != again(second) {
return (toward(again(first), PREDICT_CONTINUE_TAKEN), Predictor::Continue);
}
(Probability::even(), Predictor::Nothing)
}
fn share(
func: &Func,
cfg: &Cfg,
callees: &Callees,
returns: &[bool],
block: Block,
term: Inst,
) -> (Vec<Probability>, Predictor) {
let succs = cfg.successors(block);
if succs.is_empty() {
return (Vec::new(), Predictor::Nothing);
}
if succs.len() == 1 {
return (vec![Probability::always()], Predictor::Nothing);
}
let mut weight = vec![0u64; succs.len()];
for call in func.successors(term) {
if let Some(at) = succs.iter().position(|&block| block == call.block) {
weight[at] += 1;
}
}
let gone: Vec<bool> =
succs.iter().map(|&at| never_comes_back(func, callees, returns, at)).collect();
let total = |side: bool| -> u64 {
weight.iter().zip(&gone).filter(|&(_, &away)| away == side).map(|(w, _)| *w).sum()
};
let whole = u64::from(Probability::SCALE);
let mut parts = vec![0u32; succs.len()];
let who = if total(true) == 0 || total(false) == 0 {
hand_out(whole, &weight, &gone, total(false) == 0, &mut parts);
Predictor::Nothing
} else {
let budget = u64::from(
Probability::percent(PREDICT_NEVER_RETURNS, Quality::Guessed).complement().parts(),
);
hand_out(budget, &weight, &gone, true, &mut parts);
hand_out(whole - budget, &weight, &gone, false, &mut parts);
Predictor::NeverReturns
};
let split = parts.into_iter().map(|parts| Probability::new(parts, Quality::Guessed)).collect();
(split, who)
}
fn hand_out(budget: u64, weight: &[u64], gone: &[bool], side: bool, parts: &mut [u32]) {
let total: u64 =
weight.iter().zip(gone).filter(|&(_, &away)| away == side).map(|(w, _)| *w).sum();
if total == 0 || budget == 0 {
return;
}
let mut spent = 0;
let mut first = None;
for (at, &w) in weight.iter().enumerate() {
if gone[at] != side {
continue;
}
let share = budget * w / total;
parts[at] = u32::try_from(share).unwrap_or(Probability::SCALE);
spent += share;
if first.is_none() {
first = Some(at);
}
}
if let Some(at) = first {
parts[at] += u32::try_from(budget - spent).unwrap_or(0);
}
}
fn expect(func: &Func, cond: Value) -> Option<Probability> {
let Def::Result { inst, .. } = func[cond].def else { return None };
if func[inst].opcode != Opcode::Expect {
return None;
}
let hint = *func[func[inst].args].get(1)?;
let (value, ty) = constant(func, hint)?;
Some(toward(value.signed(ty) != 0, PREDICT_EXPECT))
}
fn pointer_null(func: &Func, cond: Value) -> Option<Probability> {
let Def::Result { inst, .. } = func[cond].def else { return None };
let data = &func[inst];
if data.opcode != Opcode::ICmp {
return None;
}
let Extra::IntPred(pred) = data.extra else { return None };
let args = &func[data.args];
let lhs = *args.first()?;
let rhs = *args.get(1)?;
if is_null(func, lhs) == is_null(func, rhs) {
return None;
}
match pred {
IntPred::Eq => Some(toward(false, PREDICT_POINTER_NOT_NULL)),
IntPred::Ne => Some(toward(true, PREDICT_POINTER_NOT_NULL)),
_ => None,
}
}
fn is_null(func: &Func, value: Value) -> bool {
if !func[value].ty.is_ptr() {
return false;
}
let Def::Result { inst, .. } = func[value].def else { return false };
if func[inst].opcode != Opcode::IntToPtr {
return false;
}
let Some(&arg) = func[func[inst].args].first() else { return false };
match constant(func, arg) {
Some((value, ty)) => value.signed(ty) == 0,
None => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Returned {
Negative,
Null,
Other,
}
fn returns_constant(func: &Func, cfg: &Cfg, start: Block) -> Option<Returned> {
let mut at = start;
for _ in 0..PREDICT_RETURN_BLOCKS {
let term = func.terminator(at)?;
if func[term].opcode == Opcode::Return {
let &value = func[func[term].args].first()?;
if is_null(func, value) {
return Some(Returned::Null);
}
let (value, ty) = constant(func, value)?;
return Some(if value.signed(ty) < 0 { Returned::Negative } else { Returned::Other });
}
match cfg.successors(at) {
[only] => at = *only,
_ => return None,
}
}
None
}
fn never_comes_back(func: &Func, callees: &Callees, returns: &[bool], block: Block) -> bool {
!returns[block.index()] || calls_named(func, block, |name| callees.never_returns(name))
}
fn calls_named(func: &Func, block: Block, mut ok: impl FnMut(Symbol) -> bool) -> bool {
func.insts(block).any(|inst| {
let data = &func[inst];
if !matches!(data.opcode, Opcode::Call | Opcode::TailCall) {
return false;
}
let Extra::Call(at) = data.extra else { return false };
match func[at].callee {
Some(name) => ok(name),
None => false,
}
})
}
fn has_call(func: &Func, block: Block) -> bool {
func.insts(block).any(|inst| {
matches!(func[inst].opcode, Opcode::Call | Opcode::TailCall | Opcode::CallIndirect)
})
}
fn enters_loop(cfg: &Cfg, loops: &Loops, from: Block, at: Block) -> bool {
if heads_a_loop(loops, from, at) {
return true;
}
match cfg.successors(at) {
[only] => heads_a_loop(loops, from, *only),
_ => false,
}
}
fn heads_a_loop(loops: &Loops, from: Block, at: Block) -> bool {
let Some(id) = loops.innermost(at) else { return false };
loops.header(id) == at && !loops.contains(id, from)
}
fn goes_round_again(loops: &Loops, from: Block, at: Block) -> bool {
match loops.innermost(from) {
Some(id) => loops.header(id) == at,
None => false,
}
}
fn returning(func: &Func, cfg: &Cfg) -> Vec<bool> {
let mut yes = vec![false; cfg.capacity()];
let mut stack = Vec::new();
for block in func.blocks() {
let Some(term) = func.terminator(block) else { continue };
if matches!(func[term].opcode, Opcode::Return | Opcode::TailCall) {
yes[block.index()] = true;
stack.push(block);
}
}
while let Some(block) = stack.pop() {
for &pred in cfg.predecessors(block) {
if !yes[pred.index()] {
yes[pred.index()] = true;
stack.push(pred);
}
}
}
yes
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
AttrSet, Attrs, Block, Builder, Func, InstData, IntPred, Opcode, Signature, Type,
};
use super::{Callees, Predictions, Predictor};
use crate::cfg::Cfg;
use crate::dom::Dominators;
use crate::loops::Loops;
use crate::profile::{Probability, Quality};
fn shape(func: &Func) -> (Cfg, Loops) {
let cfg = Cfg::new(func);
let doms = Dominators::new(&cfg);
let loops = Loops::new(&cfg, &doms);
(cfg, loops)
}
fn predict(func: &Func) -> (Predictions, Cfg) {
let (cfg, loops) = shape(func);
let seen = Predictions::of(func, &cfg, &loops, &Callees::nothing());
(seen, cfg)
}
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)
}
#[test]
fn a_block_with_one_way_out_takes_it_and_that_is_not_a_guess() {
let (_, mut func, at) = blank(2);
Builder::new(&mut func, at[0]).jump(at[1], &[]);
let mut build = Builder::new(&mut func, at[1]);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
let (seen, _) = predict(&func);
assert_eq!(seen.edges(at[0]).len(), 1);
assert_eq!(seen.taken(at[0], 0), Probability::always());
assert_eq!(seen.taken(at[0], 0).quality(), Quality::Precise);
assert!(seen.edges(at[1]).is_empty());
assert_eq!(seen.taken(at[1], 0), Probability::never());
}
#[test]
fn the_arm_that_does_not_come_back_is_the_one_not_taken() {
let (_, mut func, at) = blank(3);
let mut build = Builder::new(&mut func, at[0]);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, at[1], &[], at[2], &[]);
Builder::new(&mut func, at[1]).unreachable();
let mut build = Builder::new(&mut func, at[2]);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
let (seen, _) = predict(&func);
assert_eq!(seen.by(at[0]), Predictor::NeverReturns);
assert_eq!(seen.taken(at[0], 0), Probability::percent(99, Quality::Guessed).complement());
assert_eq!(seen.taken(at[0], 1), Probability::percent(99, Quality::Guessed));
}
#[test]
fn the_arm_that_calls_a_noreturn_function_is_the_one_not_taken() {
let (mut names, mut func, at) = blank(4);
let abort = names.intern("abort");
let sig = func.add_signature(Signature::new());
let mut build = Builder::new(&mut func, at[0]);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, at[1], &[], at[2], &[]);
let mut build = Builder::new(&mut func, at[1]);
build.call(abort, sig, &[]);
build.jump(at[3], &[]);
Builder::new(&mut func, at[2]).jump(at[3], &[]);
let mut build = Builder::new(&mut func, at[3]);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
let mut callees = Callees::nothing();
callees.record(abort, Attrs { set: AttrSet::NORETURN, ..Attrs::NONE });
let (cfg, loops) = shape(&func);
let told = Predictions::of(&func, &cfg, &loops, &callees);
assert_eq!(told.by(at[0]), Predictor::NeverReturns);
assert_eq!(told.taken(at[0], 0), Probability::percent(99, Quality::Guessed).complement());
let (guessed, _) = predict(&func);
assert_eq!(guessed.by(at[0]), Predictor::CallNotTaken);
}
#[test]
fn the_arm_that_calls_a_cold_function_is_the_one_not_taken() {
let (mut names, mut func, at) = blank(4);
let report = names.intern("report");
let sig = func.add_signature(Signature::new());
let mut build = Builder::new(&mut func, at[0]);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, at[1], &[], at[2], &[]);
let mut build = Builder::new(&mut func, at[1]);
build.call(report, sig, &[]);
build.jump(at[3], &[]);
Builder::new(&mut func, at[2]).jump(at[3], &[]);
let mut build = Builder::new(&mut func, at[3]);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
let mut callees = Callees::nothing();
callees.record(report, Attrs { set: AttrSet::COLD, ..Attrs::NONE });
let (cfg, loops) = shape(&func);
let told = Predictions::of(&func, &cfg, &loops, &callees);
assert_eq!(told.by(at[0]), Predictor::ColdCall);
assert_eq!(told.taken(at[0], 0), Probability::percent(99, Quality::Guessed).complement());
}
fn loop_shape() -> (Func, Vec<Block>) {
let (_, mut func, at) = blank(4);
Builder::new(&mut func, at[0]).jump(at[1], &[]);
let mut build = Builder::new(&mut func, at[1]);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, at[2], &[], at[3], &[]);
Builder::new(&mut func, at[2]).jump(at[1], &[]);
let mut build = Builder::new(&mut func, at[3]);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
(func, at)
}
#[test]
fn a_loop_exit_is_the_edge_not_taken() {
let (func, at) = loop_shape();
let (seen, _) = predict(&func);
assert_eq!(seen.by(at[1]), Predictor::LoopExit);
assert_eq!(seen.taken(at[1], 0), Probability::percent(89, Quality::Guessed));
assert_eq!(seen.taken(at[1], 1), Probability::percent(89, Quality::Guessed).complement());
}
#[test]
fn a_loop_guard_is_taken_more_often_than_not() {
let (_, mut func, at) = blank(6);
let mut build = Builder::new(&mut func, at[0]);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, at[1], &[], at[2], &[]);
Builder::new(&mut func, at[1]).jump(at[3], &[]);
Builder::new(&mut func, at[2]).jump(at[5], &[]);
let mut build = Builder::new(&mut func, at[3]);
let test = build.iconst(Type::int(1), 1);
build.br_if(test, at[4], &[], at[5], &[]);
Builder::new(&mut func, at[4]).jump(at[3], &[]);
let mut build = Builder::new(&mut func, at[5]);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
let (seen, _) = predict(&func);
assert_eq!(seen.by(at[0]), Predictor::LoopGuard);
assert_eq!(seen.taken(at[0], 0), Probability::percent(73, Quality::Guessed));
}
#[test]
fn a_continue_goes_round_again_more_often_than_it_falls_through() {
let (_, mut func, at) = blank(5);
Builder::new(&mut func, at[0]).jump(at[1], &[]);
let mut build = Builder::new(&mut func, at[1]);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, at[2], &[], at[3], &[]);
let mut build = Builder::new(&mut func, at[2]);
let again = build.iconst(Type::int(1), 1);
build.br_if(again, at[1], &[], at[4], &[]);
Builder::new(&mut func, at[4]).jump(at[1], &[]);
let mut build = Builder::new(&mut func, at[3]);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
let (seen, _) = predict(&func);
assert_eq!(seen.by(at[2]), Predictor::Continue);
assert_eq!(seen.taken(at[2], 0), Probability::percent(67, Quality::Guessed));
}
#[test]
fn a_pointer_tested_against_null_is_predicted_not_null() {
let (_, mut func, at) = blank(3);
let mut build = Builder::new(&mut func, at[0]);
let seven = build.iconst(Type::int(64), 7);
let some = build.unary(Opcode::IntToPtr, seven, Type::PTR);
let zero = build.iconst(Type::int(64), 0);
let null = build.unary(Opcode::IntToPtr, zero, Type::PTR);
let cond = build.icmp(IntPred::Eq, some, null);
build.br_if(cond, at[1], &[], at[2], &[]);
for block in [at[1], at[2]] {
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
}
let (seen, _) = predict(&func);
assert_eq!(seen.by(at[0]), Predictor::PointerNotNull);
assert_eq!(seen.taken(at[0], 0), Probability::percent(70, Quality::Guessed).complement());
}
#[test]
fn an_arm_that_returns_a_negative_number_is_the_one_not_taken() {
let (_, mut func, at) = blank(3);
let mut build = Builder::new(&mut func, at[0]);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, at[1], &[], at[2], &[]);
let mut build = Builder::new(&mut func, at[1]);
let bad = build.iconst(Type::int(32), -1);
build.ret(&[bad]);
let mut build = Builder::new(&mut func, at[2]);
let good = build.iconst(Type::int(32), 0);
build.ret(&[good]);
let (seen, _) = predict(&func);
assert_eq!(seen.by(at[0]), Predictor::NegativeReturn);
assert_eq!(seen.taken(at[0], 0), Probability::percent(98, Quality::Guessed).complement());
}
#[test]
fn an_arm_that_returns_null_is_the_one_not_taken_and_by_a_smaller_margin() {
let (_, mut func, at) = blank(3);
let mut build = Builder::new(&mut func, at[0]);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, at[1], &[], at[2], &[]);
let mut build = Builder::new(&mut func, at[1]);
let zero = build.iconst(Type::int(64), 0);
let null = build.unary(Opcode::IntToPtr, zero, Type::PTR);
build.ret(&[null]);
let mut build = Builder::new(&mut func, at[2]);
let seven = build.iconst(Type::int(64), 7);
let some = build.unary(Opcode::IntToPtr, seven, Type::PTR);
build.ret(&[some]);
let (seen, _) = predict(&func);
assert_eq!(seen.by(at[0]), Predictor::NullReturn);
assert_eq!(seen.taken(at[0], 0), Probability::percent(71, Quality::Guessed).complement());
assert!(Predictor::NullReturn.hit_rate() < Predictor::NegativeReturn.hit_rate());
}
#[test]
fn nothing_to_go_on_is_an_even_split_that_says_it_is_a_guess() {
let (_, mut func, at) = blank(3);
let mut build = Builder::new(&mut func, at[0]);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, at[1], &[], at[2], &[]);
for block in [at[1], at[2]] {
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
}
let (seen, _) = predict(&func);
assert_eq!(seen.by(at[0]), Predictor::Nothing);
assert_eq!(seen.taken(at[0], 0), Probability::even());
assert_eq!(seen.taken(at[0], 0).quality(), Quality::Guessed);
assert!(!seen.taken(at[0], 0).is_predictable());
}
#[test]
fn a_builtin_expect_wins_over_every_predictor_after_it() {
let (_, mut func, at) = blank(3);
let mut build = Builder::new(&mut func, at[0]);
let value = build.iconst(Type::int(1), 1);
let hint = build.iconst(Type::int(1), 1);
let args = build.func().push_values(&[value, hint]);
let cond = build.value(InstData { args, ..InstData::new(Opcode::Expect) }, Type::int(1));
build.br_if(cond, at[1], &[], at[2], &[]);
Builder::new(&mut func, at[1]).unreachable();
let mut build = Builder::new(&mut func, at[2]);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
let (seen, _) = predict(&func);
assert_eq!(seen.by(at[0]), Predictor::Expect);
assert_eq!(seen.taken(at[0], 0), Probability::percent(90, Quality::Guessed));
}
#[test]
fn a_builtin_expect_of_zero_names_the_other_arm() {
let (_, mut func, at) = blank(3);
let mut build = Builder::new(&mut func, at[0]);
let value = build.iconst(Type::int(1), 1);
let hint = build.iconst(Type::int(1), 0);
let args = build.func().push_values(&[value, hint]);
let cond = build.value(InstData { args, ..InstData::new(Opcode::Expect) }, Type::int(1));
build.br_if(cond, at[1], &[], at[2], &[]);
for block in [at[1], at[2]] {
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
}
let (seen, _) = predict(&func);
assert_eq!(seen.by(at[0]), Predictor::Expect);
assert_eq!(seen.taken(at[0], 0), Probability::percent(90, Quality::Guessed).complement());
}
fn switch_shape() -> (Func, Vec<Block>) {
let (_, mut func, at) = blank(5);
let mut build = Builder::new(&mut func, at[0]);
let value = build.iconst(Type::int(32), 0);
build.switch(value, at[1], &[(0, at[2]), (1, at[3]), (2, at[4]), (3, at[4])]);
Builder::new(&mut func, at[2]).unreachable();
for block in [at[1], at[3], at[4]] {
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
}
(func, at)
}
#[test]
fn a_switch_arm_that_aborts_leaves_the_rest_to_share_what_is_left() {
let (func, at) = switch_shape();
let (seen, cfg) = predict(&func);
let succs = cfg.successors(at[0]);
let aborts = succs.iter().position(|&block| block == at[2]).expect("the arm is an edge");
let shared = succs.iter().position(|&block| block == at[4]).expect("the arm is an edge");
let alone = succs.iter().position(|&block| block == at[3]).expect("the arm is an edge");
assert_eq!(seen.by(at[0]), Predictor::NeverReturns);
assert_eq!(
seen.taken(at[0], aborts),
Probability::percent(99, Quality::Guessed).complement()
);
assert_eq!(seen.taken(at[0], shared).parts(), 2 * seen.taken(at[0], alone).parts());
}
#[test]
fn the_edges_out_of_every_block_add_up_to_certainty() {
let (guarded, _) = {
let (_, mut func, at) = blank(3);
let mut build = Builder::new(&mut func, at[0]);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, at[1], &[], at[2], &[]);
for block in [at[1], at[2]] {
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
}
(func, at)
};
let (looped, _) = loop_shape();
let (switched, _) = switch_shape();
for func in [guarded, looped, switched] {
let (seen, cfg) = predict(&func);
for block in func.blocks() {
let edges = seen.edges(block);
if edges.is_empty() {
continue;
}
assert_eq!(edges.len(), cfg.successors(block).len());
let total: u32 = edges.iter().map(|edge| edge.parts()).sum();
assert_eq!(total, Probability::SCALE, "block {block:?} does not add up");
}
}
}
#[test]
fn the_ten_are_the_ten_the_document_named_and_they_are_asked_in_its_order() {
assert_eq!(Predictor::ORDER.len(), 10);
assert!(!Predictor::ORDER.contains(&Predictor::Nothing));
let mut sorted = Predictor::ORDER;
sorted.sort_unstable();
assert_eq!(sorted, Predictor::ORDER, "the enum order is the order they are asked in");
for one in Predictor::ORDER {
assert!(one.hit_rate() > Predictor::Nothing.hit_rate(), "{one} predicts nothing");
assert!(!one.as_str().is_empty());
}
}
}