use std::fmt;
use rucc_cost::heuristics::HOT_BLOCK_FRACTION;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Quality {
Unknown,
Guessed,
Adjusted,
Precise,
}
impl Quality {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Unknown => "unknown",
Self::Guessed => "guessed",
Self::Adjusted => "adjusted",
Self::Precise => "precise",
}
}
#[must_use]
pub const fn is_measured(self) -> bool {
matches!(self, Self::Adjusted | Self::Precise)
}
}
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Probability {
parts: u32,
quality: Quality,
}
impl Probability {
pub const SCALE: u32 = 10_000;
#[must_use]
pub const fn new(parts: u32, quality: Quality) -> Self {
let parts = if parts > Self::SCALE { Self::SCALE } else { parts };
Self { parts, quality }
}
#[must_use]
pub const fn percent(percent: u32, quality: Quality) -> Self {
Self::new(percent.saturating_mul(Self::SCALE / 100), quality)
}
#[must_use]
pub const fn always() -> Self {
Self { parts: Self::SCALE, quality: Quality::Precise }
}
#[must_use]
pub const fn never() -> Self {
Self { parts: 0, quality: Quality::Precise }
}
#[must_use]
pub const fn even() -> Self {
Self { parts: Self::SCALE / 2, quality: Quality::Guessed }
}
#[must_use]
pub const fn parts(self) -> u32 {
self.parts
}
#[must_use]
pub const fn quality(self) -> Quality {
self.quality
}
#[must_use]
pub const fn complement(self) -> Self {
Self { parts: Self::SCALE - self.parts, quality: self.quality }
}
#[must_use]
pub fn and(self, other: Self) -> Self {
let parts = u64::from(self.parts) * u64::from(other.parts) / u64::from(Self::SCALE);
Self { parts: parts as u32, quality: self.quality.min(other.quality) }
}
#[must_use]
pub fn is_predictable(self) -> bool {
if !self.quality.is_measured() {
return false;
}
let margin = rucc_cost::heuristics::PREDICTABLE_BRANCH_PERCENT * (Self::SCALE / 100);
self.parts <= margin || self.parts >= Self::SCALE - margin
}
}
impl fmt::Display for Probability {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let whole = self.parts / (Self::SCALE / 100);
let rest = self.parts % (Self::SCALE / 100);
if rest == 0 { write!(f, "{whole}%") } else { write!(f, "{whole}.{rest:02}%") }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Frequency {
scaled: u64,
quality: Quality,
}
impl Frequency {
pub const ENTRY: Self = Self { scaled: Probability::SCALE as u64, quality: Quality::Precise };
pub const NEVER: Self = Self { scaled: 0, quality: Quality::Precise };
pub const UNKNOWN: Self = Self { scaled: 0, quality: Quality::Unknown };
pub const MAX: Self = Self { scaled: u64::MAX, quality: Quality::Guessed };
#[must_use]
pub const fn times(count: u32, quality: Quality) -> Self {
Self { scaled: (count as u64).saturating_mul(Probability::SCALE as u64), quality }
}
#[must_use]
pub const fn raw(self) -> u64 {
self.scaled
}
#[must_use]
pub const fn quality(self) -> Quality {
self.quality
}
#[must_use]
pub const fn is_saturated(self) -> bool {
self.scaled == u64::MAX
}
#[must_use]
pub fn along(self, edge: Probability) -> Self {
let scaled =
(u128::from(self.scaled) * u128::from(edge.parts())) / u128::from(Probability::SCALE);
Self {
scaled: u64::try_from(scaled).unwrap_or(u64::MAX),
quality: self.quality.min(edge.quality()),
}
}
#[must_use]
pub fn plus(self, other: Self) -> Self {
Self {
scaled: self.scaled.saturating_add(other.scaled),
quality: self.quality.min(other.quality),
}
}
#[must_use]
pub fn repeated(self, iterations: u32) -> Self {
Self {
scaled: self.scaled.saturating_mul(u64::from(iterations)),
quality: self.quality.min(Quality::Guessed),
}
}
#[must_use]
pub fn repeated_while(self, again: Probability, cap: u32) -> Self {
let scale = u64::from(Probability::SCALE);
let stop = scale - u64::from(again.parts().min(Probability::SCALE));
let floor = scale.div_ceil(u64::from(cap.max(1)));
let stop = stop.max(floor);
let scaled = (u128::from(self.scaled) * u128::from(scale)) / u128::from(stop);
Self {
scaled: u64::try_from(scaled).unwrap_or(u64::MAX),
quality: self.quality.min(again.quality()),
}
}
#[must_use]
pub fn is_hot_in_function(self, entry: Self) -> bool {
if entry.scaled == 0 {
return false;
}
self.scaled >= entry.scaled.div_ceil(u64::from(HOT_BLOCK_FRACTION))
}
#[must_use]
pub const fn is_hot_in_program(self) -> Hotness {
Hotness::Unknown
}
}
impl fmt::Display for Frequency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_saturated() {
return write!(f, "saturated ({})", self.quality);
}
let scale = u64::from(Probability::SCALE);
let whole = self.scaled / scale;
let rest = (self.scaled % scale) / (scale / 100);
write!(f, "{whole}.{rest:02} ({})", self.quality)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Hotness {
Hot,
Cold,
Unknown,
}
#[cfg(test)]
mod tests {
use super::{Frequency, Hotness, Probability, Quality};
#[test]
fn the_qualities_are_ordered_worst_first_so_the_minimum_is_the_degraded_one() {
assert!(Quality::Unknown < Quality::Guessed);
assert!(Quality::Guessed < Quality::Adjusted);
assert!(Quality::Adjusted < Quality::Precise);
assert!(!Quality::Guessed.is_measured());
assert!(Quality::Adjusted.is_measured());
}
#[test]
fn a_probability_above_certainty_is_certainty() {
let over = Probability::new(Probability::SCALE + 1, Quality::Guessed);
assert_eq!(over.parts(), Probability::SCALE);
assert_eq!(Probability::percent(200, Quality::Guessed).parts(), Probability::SCALE);
}
#[test]
fn the_two_edges_out_of_a_branch_add_up_to_one() {
let taken = Probability::percent(89, Quality::Guessed);
assert_eq!(taken.parts() + taken.complement().parts(), Probability::SCALE);
assert_eq!(taken.complement().complement(), taken);
assert_eq!(taken.complement().quality(), Quality::Guessed);
}
#[test]
fn a_measurement_combined_with_a_guess_comes_out_a_guess() {
let measured = Probability::percent(50, Quality::Precise);
let guessed = Probability::percent(50, Quality::Guessed);
let both = measured.and(guessed);
assert_eq!(both.parts(), Probability::SCALE / 4);
assert_eq!(both.quality(), Quality::Guessed);
}
#[test]
fn a_statically_predicted_branch_is_never_predictable_however_extreme_it_is() {
assert!(!Probability::percent(99, Quality::Guessed).is_predictable());
assert!(Probability::percent(99, Quality::Precise).is_predictable());
assert!(!Probability::percent(90, Quality::Precise).is_predictable());
assert!(Probability::percent(1, Quality::Adjusted).is_predictable());
}
#[test]
fn a_frequency_carried_along_an_edge_takes_the_worse_of_the_two_qualities() {
let ten = Frequency::times(10, Quality::Precise);
let along = ten.along(Probability::percent(30, Quality::Guessed));
assert_eq!(along.raw(), 3 * u64::from(Probability::SCALE));
assert_eq!(along.quality(), Quality::Guessed);
}
#[test]
fn the_arithmetic_saturates_rather_than_wrapping() {
assert!(Frequency::MAX.plus(Frequency::ENTRY).is_saturated());
assert!(Frequency::MAX.repeated(2).is_saturated());
assert!(Frequency::times(u32::MAX, Quality::Guessed).repeated(u32::MAX).is_saturated());
assert_eq!(Frequency::MAX.along(Probability::always()).raw(), u64::MAX);
}
#[test]
fn hot_in_a_function_is_one_part_in_a_thousand_of_the_entry() {
let entry = Frequency::ENTRY;
let thousandth = Frequency { scaled: entry.raw() / 1000, quality: Quality::Guessed };
let less = Frequency { scaled: entry.raw() / 1000 - 1, quality: Quality::Guessed };
assert!(thousandth.is_hot_in_function(entry));
assert!(!less.is_hot_in_function(entry));
assert!(Frequency::times(10, Quality::Guessed).is_hot_in_function(entry));
assert!(!Frequency::NEVER.is_hot_in_function(entry));
}
#[test]
fn nothing_is_hot_in_a_function_that_never_runs() {
assert!(!Frequency::ENTRY.is_hot_in_function(Frequency::NEVER));
}
#[test]
fn hot_in_the_program_says_it_does_not_know_even_about_a_measured_frequency() {
assert_eq!(Frequency::ENTRY.is_hot_in_program(), Hotness::Unknown);
assert_eq!(Frequency::times(1000, Quality::Precise).is_hot_in_program(), Hotness::Unknown);
}
#[test]
fn what_a_dump_shows() {
assert_eq!(Probability::percent(73, Quality::Guessed).to_string(), "73%");
assert_eq!(Probability::new(7345, Quality::Guessed).to_string(), "73.45%");
assert_eq!(Probability::always().to_string(), "100%");
assert_eq!(Frequency::ENTRY.to_string(), "1.00 (precise)");
assert_eq!(Frequency::UNKNOWN.to_string(), "0.00 (unknown)");
assert_eq!(Frequency::times(12, Quality::Guessed).to_string(), "12.00 (guessed)");
assert_eq!(Frequency::MAX.to_string(), "saturated (guessed)");
}
}