Skip to main content

rucc_opt/
profile.rs

1//! How likely an edge is taken, how often a block runs, and how much either is worth believing.
2//!
3//! Design: `spec/optimizer/11-profile-and-frequency.md`.
4//!
5//! Almost every cost decision in the optimizer is a form of the question "is this code hot", and
6//! this is where the answer comes from. Inlining, unrolling, block layout, spill placement and
7//! if-conversion all read block frequency, so a frequency that is wrong makes all of them wrong
8//! together in a way that is very hard to attribute to anything.
9//!
10//! # A number is not enough
11//!
12//! Section 11.1 calls the provenance field the best idea in GCC's profile machinery, and this
13//! module is built around it. A [`Probability`] and a [`Frequency`] each carry a [`Quality`]
14//! saying where the number came from, arithmetic on them degrades the quality to the worse of the
15//! two inputs, and there is no way to build either one without saying what its quality is.
16//!
17//! The reason is what happens without it. A compilation with measured data for half a program and
18//! guesses for the other half computes with both constantly, and one guess laundered through three
19//! arithmetic operations comes out indistinguishable from a measurement. The inliner then makes an
20//! aggressive decision on a fabricated number, and nothing anywhere says so. With the quality on
21//! the value, a consumer that should behave differently on a guess can ask, and one that forgot to
22//! ask is at least reading a number whose history is still attached to it.
23//!
24//! The same rule runs in the other direction, which is section 11.6: a static predictor may only
25//! write a probability whose quality is [`Quality::Guessed`], and only where what is already there
26//! is worth less. A heuristic that overrides a measurement is a heuristic that is wrong by
27//! construction, because the measurement is the thing the heuristic is trying to approximate.
28//!
29//! # Fixed point, and saturating
30//!
31//! Both types are scaled integers rather than floats. Section 11.3 asks for that and
32//! `spec/03-architecture.md`'s determinism rule is why: a float result depends on evaluation order
33//! and on the host's excess precision, frequencies feed cost comparisons, and cost comparisons
34//! decide what code comes out. A frequency that differs in the last bit between two hosts is a
35//! reproducibility failure rather than a rounding question.
36//!
37//! Nested loops multiply, so a frequency deep in a loop nest grows fast. Every operation here
38//! saturates, and it saturates in the type rather than at the call sites, which is section 11.6's
39//! second failure mode: a saturation that each caller is responsible for is a saturation that one
40//! caller forgets.
41
42use std::fmt;
43
44use rucc_cost::heuristics::HOT_BLOCK_FRACTION;
45
46/// How much a probability or a frequency is worth believing.
47///
48/// The order is the point, and it is GCC's order out of `enum profile_quality` in
49/// `gcc/profile-count.h`: worse first, so the quality of a computed value is the smaller of what
50/// went into it and `Ord` says so without a table.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
52pub enum Quality {
53    /// Nobody has said anything about this one.
54    ///
55    /// What a block a pass created gets until the pass says otherwise. It is not zero and it is
56    /// not one, it is the absence of a claim, and a consumer that treats it as either is the
57    /// third failure mode in section 11.6.
58    Unknown,
59
60    /// A static predictor said so, from the shape of the code and nothing else.
61    ///
62    /// Everything in M4 is this, because there is no profile data yet. A guess is still much
63    /// better than nothing: the hit rates in section 11.2 are measurements of how people write
64    /// programs, and those have held up for thirty years.
65    Guessed,
66
67    /// It came from a measurement, and then a transformation scaled it.
68    ///
69    /// Splitting a block, unrolling a loop or threading a jump all divide a measured count across
70    /// paths that were not measured separately. The result is worth more than a guess and less
71    /// than what was measured, which is exactly what this says.
72    Adjusted,
73
74    /// Measured, and nothing has touched it since.
75    ///
76    /// Also what a branch on a constant gets, because that one is not a measurement or a guess. It
77    /// is arithmetic.
78    Precise,
79}
80
81impl Quality {
82    /// How the quality reads in a dump.
83    #[must_use]
84    pub const fn as_str(self) -> &'static str {
85        match self {
86            Self::Unknown => "unknown",
87            Self::Guessed => "guessed",
88            Self::Adjusted => "adjusted",
89            Self::Precise => "precise",
90        }
91    }
92
93    /// Whether this came from running the program rather than from looking at it.
94    ///
95    /// The question a consumer asks when it is about to do something it could not undo. Section
96    /// 40.5's rule that a statically predicted branch never counts as predictable is this
97    /// predicate, and it is here rather than at each call site so that the rule is one thing.
98    #[must_use]
99    pub const fn is_measured(self) -> bool {
100        matches!(self, Self::Adjusted | Self::Precise)
101    }
102}
103
104impl fmt::Display for Quality {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        f.write_str(self.as_str())
107    }
108}
109
110/// How likely one edge out of a block is taken, and how much that is worth believing.
111///
112/// Held as parts of [`Probability::SCALE`], which is ten thousand, so a hit rate written as a
113/// whole percent is exact and one written to two decimal places is too. GCC's `REG_BR_PROB_BASE`
114/// is the same idea at the same size.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
116pub struct Probability {
117    parts: u32,
118    quality: Quality,
119}
120
121impl Probability {
122    /// What a probability is out of.
123    pub const SCALE: u32 = 10_000;
124
125    /// A probability of `parts` out of [`Probability::SCALE`], believed this much.
126    ///
127    /// There is no constructor that does not say where the number came from, which is section
128    /// 11.6's second failure mode closed off at the type. More than the scale is not a
129    /// probability, and it is clamped rather than refused, because the callers that can produce
130    /// one are all doing arithmetic where the answer is certainty.
131    #[must_use]
132    pub const fn new(parts: u32, quality: Quality) -> Self {
133        let parts = if parts > Self::SCALE { Self::SCALE } else { parts };
134        Self { parts, quality }
135    }
136
137    /// A hit rate written as a whole percentage, which is how section 11.2 writes all of them.
138    #[must_use]
139    pub const fn percent(percent: u32, quality: Quality) -> Self {
140        Self::new(percent.saturating_mul(Self::SCALE / 100), quality)
141    }
142
143    /// The edge is always taken, and that is arithmetic rather than a guess.
144    #[must_use]
145    pub const fn always() -> Self {
146        Self { parts: Self::SCALE, quality: Quality::Precise }
147    }
148
149    /// The edge is never taken.
150    #[must_use]
151    pub const fn never() -> Self {
152        Self { parts: 0, quality: Quality::Precise }
153    }
154
155    /// Nothing is known about this edge, so it is even and says so.
156    ///
157    /// The starting point for a two way branch no predictor matched. Even and guessed is a
158    /// different statement from even and measured, and the second one is a real fact about a
159    /// branch that is genuinely unpredictable.
160    #[must_use]
161    pub const fn even() -> Self {
162        Self { parts: Self::SCALE / 2, quality: Quality::Guessed }
163    }
164
165    /// The parts out of [`Probability::SCALE`].
166    #[must_use]
167    pub const fn parts(self) -> u32 {
168        self.parts
169    }
170
171    /// How much this is worth believing.
172    #[must_use]
173    pub const fn quality(self) -> Quality {
174        self.quality
175    }
176
177    /// The other edge out of the same branch.
178    #[must_use]
179    pub const fn complement(self) -> Self {
180        Self { parts: Self::SCALE - self.parts, quality: self.quality }
181    }
182
183    /// Both, for an edge reached by taking this one and then that one.
184    ///
185    /// The quality is the worse of the two, which is the whole reason these are not bare numbers.
186    #[must_use]
187    pub fn and(self, other: Self) -> Self {
188        let parts = u64::from(self.parts) * u64::from(other.parts) / u64::from(Self::SCALE);
189        // The product of two things at most the scale is at most the scale, so the cast is exact.
190        Self { parts: parts as u32, quality: self.quality.min(other.quality) }
191    }
192
193    /// Whether a branch this likely one way is one a machine will predict correctly.
194    ///
195    /// Section 40.5, and the part of it that matters is not the threshold. A probability a static
196    /// predictor guessed never counts as predictable however extreme it is, because the branch
197    /// predictor in the machine is looking at what the program does and the predictor here is
198    /// looking at what the program says. Guessing that a loop exit is not taken 89 times in 100 is
199    /// not evidence about any particular branch.
200    #[must_use]
201    pub fn is_predictable(self) -> bool {
202        if !self.quality.is_measured() {
203            return false;
204        }
205        let margin = rucc_cost::heuristics::PREDICTABLE_BRANCH_PERCENT * (Self::SCALE / 100);
206        self.parts <= margin || self.parts >= Self::SCALE - margin
207    }
208}
209
210impl fmt::Display for Probability {
211    /// As a percentage, with the two decimal places only when they say something.
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        let whole = self.parts / (Self::SCALE / 100);
214        let rest = self.parts % (Self::SCALE / 100);
215        if rest == 0 { write!(f, "{whole}%") } else { write!(f, "{whole}.{rest:02}%") }
216    }
217}
218
219/// How often a block runs, relative to one entry to the function it is in.
220///
221/// The entry block is [`Frequency::ENTRY`], which is one. A block inside a loop predicted to run
222/// ten times is ten. A block on an error path is a fraction. The unit is deliberately relative:
223/// how often this block runs compared to the whole function is a question that can be answered
224/// without a profile, and how often it runs compared to the rest of the program cannot.
225#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
226pub struct Frequency {
227    /// Scaled by [`Probability::SCALE`], so that a frequency and a probability are the same
228    /// fixed point and multiplying one by the other is exact.
229    scaled: u64,
230    quality: Quality,
231}
232
233impl Frequency {
234    /// One execution per entry to the function, which is what the entry block gets.
235    ///
236    /// Precise, because it is not a claim about the program. It is the definition of the unit.
237    pub const ENTRY: Self = Self { scaled: Probability::SCALE as u64, quality: Quality::Precise };
238
239    /// The block does not run at all.
240    pub const NEVER: Self = Self { scaled: 0, quality: Quality::Precise };
241
242    /// Nobody has computed one for this block yet.
243    ///
244    /// Zero, so that a consumer that ignores the quality is at least conservative rather than
245    /// wrong in the direction that puts cold code in the hot section. The quality is what says the
246    /// zero means nothing.
247    pub const UNKNOWN: Self = Self { scaled: 0, quality: Quality::Unknown };
248
249    /// As high as this goes, which is what everything saturates to.
250    ///
251    /// A frequency here means the arithmetic ran out of room, which nested loops will do to any
252    /// fixed size number. What it must not do is wrap, because a hot block that comes out cold is
253    /// a decision nobody can explain afterwards.
254    pub const MAX: Self = Self { scaled: u64::MAX, quality: Quality::Guessed };
255
256    /// Runs this many times per entry to the function, believed this much.
257    #[must_use]
258    pub const fn times(count: u32, quality: Quality) -> Self {
259        Self { scaled: (count as u64).saturating_mul(Probability::SCALE as u64), quality }
260    }
261
262    /// The raw fixed point value, scaled by [`Probability::SCALE`].
263    ///
264    /// For a dump or a comparison. A consumer doing arithmetic on this rather than on the
265    /// [`Frequency`] is a consumer that has dropped the quality on the floor.
266    #[must_use]
267    pub const fn raw(self) -> u64 {
268        self.scaled
269    }
270
271    /// How much this is worth believing.
272    #[must_use]
273    pub const fn quality(self) -> Quality {
274        self.quality
275    }
276
277    /// Whether the arithmetic ran out of room getting here.
278    #[must_use]
279    pub const fn is_saturated(self) -> bool {
280        self.scaled == u64::MAX
281    }
282
283    /// This block's frequency carried along an edge taken this often.
284    #[must_use]
285    pub fn along(self, edge: Probability) -> Self {
286        let scaled =
287            (u128::from(self.scaled) * u128::from(edge.parts())) / u128::from(Probability::SCALE);
288        Self {
289            scaled: u64::try_from(scaled).unwrap_or(u64::MAX),
290            quality: self.quality.min(edge.quality()),
291        }
292    }
293
294    /// Two paths into the same block.
295    #[must_use]
296    pub fn plus(self, other: Self) -> Self {
297        Self {
298            scaled: self.scaled.saturating_add(other.scaled),
299            quality: self.quality.min(other.quality),
300        }
301    }
302
303    /// This block, once per iteration of a loop that runs `iterations` times.
304    ///
305    /// The caller clamps the iteration count before it gets here, per section 11.2. Saturating
306    /// multiplication keeps the arithmetic honest, but a nest of loops each claimed to run four
307    /// billion times has already lost the argument somewhere further up.
308    #[must_use]
309    pub fn repeated(self, iterations: u32) -> Self {
310        Self {
311            scaled: self.scaled.saturating_mul(u64::from(iterations)),
312            quality: self.quality.min(Quality::Guessed),
313        }
314    }
315
316    /// This block's frequency once the loop it heads has gone round as often as it is going to.
317    ///
318    /// The header of a loop runs once for the iteration that enters it and again for every
319    /// iteration that goes back to it, so if `again` is the probability of going round, the header
320    /// runs `1 / (1 - again)` times for each entry. That is the sum of the geometric series and it
321    /// is the whole of Wu and Larus's method in one line, which is why section 11.3 asks for it
322    /// rather than for an iteration of the linear system until it settles.
323    ///
324    /// Two things have to be true of `again` or this produces nonsense, and both are handled here
325    /// rather than in the caller, because a division by nearly zero is the most common way a
326    /// frequency implementation breaks. A loop with no predicted exit has `again` at certainty and
327    /// the series does not converge, and one with an `again` a hair below certainty converges on a
328    /// number no machine will run. So the count is capped at `cap` iterations, which is section
329    /// 11.2's `max-predicted-iterations`, and the cap is what a loop whose exit nothing predicted
330    /// gets.
331    #[must_use]
332    pub fn repeated_while(self, again: Probability, cap: u32) -> Self {
333        let scale = u64::from(Probability::SCALE);
334        // What is left of certainty, which is how likely the loop is to stop this time round. The
335        // cap is a floor under it: stopping one time in a hundred is a hundred iterations.
336        let stop = scale - u64::from(again.parts().min(Probability::SCALE));
337        let floor = scale.div_ceil(u64::from(cap.max(1)));
338        let stop = stop.max(floor);
339        let scaled = (u128::from(self.scaled) * u128::from(scale)) / u128::from(stop);
340        Self {
341            scaled: u64::try_from(scaled).unwrap_or(u64::MAX),
342            quality: self.quality.min(again.quality()),
343        }
344    }
345
346    /// Whether this block is hot compared with the rest of the function it is in.
347    ///
348    /// Section 11.4, and GCC's `hot-bb-frequency-fraction`: at least one part in
349    /// [`HOT_BLOCK_FRACTION`] of the entry block. This is the question the register allocator and
350    /// the loop passes are asking, and it is answerable with no profile at all, because it is a
351    /// comparison between two blocks that were predicted the same way.
352    ///
353    /// An entry frequency of zero is not a scale to be hot against, so nothing is hot in a
354    /// function that never runs.
355    #[must_use]
356    pub fn is_hot_in_function(self, entry: Self) -> bool {
357        if entry.scaled == 0 {
358            return false;
359        }
360        self.scaled >= entry.scaled.div_ceil(u64::from(HOT_BLOCK_FRACTION))
361    }
362
363    /// Whether this block is hot compared with the whole program.
364    ///
365    /// A different question from [`Frequency::is_hot_in_function`], which is why section 11.4 asks
366    /// for two predicates named so they cannot be confused. The section placement decision wants
367    /// this one: a block that runs a thousand times per call in a function called twice is hot in
368    /// its function and cold in the program.
369    ///
370    /// It is [`Hotness::Unknown`] today and will be until there is whole program profile data,
371    /// which is document 35 and is after M4. The predicate exists now so that every caller is
372    /// written against three answers from the start. A boolean that quietly means "hot, or we have
373    /// no idea" is how cold code ends up in the hot section, and retrofitting the third answer
374    /// into callers written against two is the part that does not happen.
375    #[must_use]
376    pub const fn is_hot_in_program(self) -> Hotness {
377        Hotness::Unknown
378    }
379}
380
381impl fmt::Display for Frequency {
382    /// As a multiple of the entry, to two decimal places, with the quality after it.
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        if self.is_saturated() {
385            return write!(f, "saturated ({})", self.quality);
386        }
387        let scale = u64::from(Probability::SCALE);
388        let whole = self.scaled / scale;
389        let rest = (self.scaled % scale) / (scale / 100);
390        write!(f, "{whole}.{rest:02} ({})", self.quality)
391    }
392}
393
394/// What a hotness question can answer.
395///
396/// Three answers rather than two, because the third one is real. Section 11.4 is specific that a
397/// consumer has to handle it explicitly rather than folding it into either of the others, and the
398/// two directions it could be folded are both wrong: treating unknown as hot puts cold code in the
399/// hot section, and treating it as cold puts the hot path there instead.
400#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
401pub enum Hotness {
402    /// It runs often enough to spend on.
403    Hot,
404    /// It does not.
405    Cold,
406    /// There is no data that answers this, and there is no defensible guess either.
407    Unknown,
408}
409
410#[cfg(test)]
411mod tests {
412    use super::{Frequency, Hotness, Probability, Quality};
413
414    #[test]
415    fn the_qualities_are_ordered_worst_first_so_the_minimum_is_the_degraded_one() {
416        assert!(Quality::Unknown < Quality::Guessed);
417        assert!(Quality::Guessed < Quality::Adjusted);
418        assert!(Quality::Adjusted < Quality::Precise);
419        assert!(!Quality::Guessed.is_measured());
420        assert!(Quality::Adjusted.is_measured());
421    }
422
423    #[test]
424    fn a_probability_above_certainty_is_certainty() {
425        let over = Probability::new(Probability::SCALE + 1, Quality::Guessed);
426        assert_eq!(over.parts(), Probability::SCALE);
427        assert_eq!(Probability::percent(200, Quality::Guessed).parts(), Probability::SCALE);
428    }
429
430    #[test]
431    fn the_two_edges_out_of_a_branch_add_up_to_one() {
432        let taken = Probability::percent(89, Quality::Guessed);
433        assert_eq!(taken.parts() + taken.complement().parts(), Probability::SCALE);
434        assert_eq!(taken.complement().complement(), taken);
435        // The complement of a guess is a guess. Knowing an edge is unlikely because a heuristic
436        // said the other one was likely is still the heuristic talking.
437        assert_eq!(taken.complement().quality(), Quality::Guessed);
438    }
439
440    #[test]
441    fn a_measurement_combined_with_a_guess_comes_out_a_guess() {
442        // This is the whole point of the quality field. Without it the product below is a number
443        // that looks exactly like a measurement of a path nobody ever measured.
444        let measured = Probability::percent(50, Quality::Precise);
445        let guessed = Probability::percent(50, Quality::Guessed);
446        let both = measured.and(guessed);
447        assert_eq!(both.parts(), Probability::SCALE / 4);
448        assert_eq!(both.quality(), Quality::Guessed);
449    }
450
451    #[test]
452    fn a_statically_predicted_branch_is_never_predictable_however_extreme_it_is() {
453        // Section 40.5, and the reason if-conversion has to ask. A predictor saying a loop exit is
454        // taken once in a hundred is a statement about loops, not about this branch, and the
455        // machine's branch predictor has the actual history.
456        assert!(!Probability::percent(99, Quality::Guessed).is_predictable());
457        assert!(Probability::percent(99, Quality::Precise).is_predictable());
458        assert!(!Probability::percent(90, Quality::Precise).is_predictable());
459        assert!(Probability::percent(1, Quality::Adjusted).is_predictable());
460    }
461
462    #[test]
463    fn a_frequency_carried_along_an_edge_takes_the_worse_of_the_two_qualities() {
464        let ten = Frequency::times(10, Quality::Precise);
465        let along = ten.along(Probability::percent(30, Quality::Guessed));
466        assert_eq!(along.raw(), 3 * u64::from(Probability::SCALE));
467        assert_eq!(along.quality(), Quality::Guessed);
468    }
469
470    #[test]
471    fn the_arithmetic_saturates_rather_than_wrapping() {
472        // Nested loops multiply, and a hot block that wraps to cold is a decision nobody can
473        // explain afterwards. Every operation has to hold this, not just the one that overflowed
474        // in whatever test was written the day it was noticed.
475        assert!(Frequency::MAX.plus(Frequency::ENTRY).is_saturated());
476        assert!(Frequency::MAX.repeated(2).is_saturated());
477        assert!(Frequency::times(u32::MAX, Quality::Guessed).repeated(u32::MAX).is_saturated());
478        // Along an edge is the one direction that cannot overflow, since a probability is at most
479        // one, and it still must not lose the top of the range on the way through.
480        assert_eq!(Frequency::MAX.along(Probability::always()).raw(), u64::MAX);
481    }
482
483    #[test]
484    fn hot_in_a_function_is_one_part_in_a_thousand_of_the_entry() {
485        let entry = Frequency::ENTRY;
486        let thousandth = Frequency { scaled: entry.raw() / 1000, quality: Quality::Guessed };
487        let less = Frequency { scaled: entry.raw() / 1000 - 1, quality: Quality::Guessed };
488        assert!(thousandth.is_hot_in_function(entry));
489        assert!(!less.is_hot_in_function(entry));
490        assert!(Frequency::times(10, Quality::Guessed).is_hot_in_function(entry));
491        assert!(!Frequency::NEVER.is_hot_in_function(entry));
492    }
493
494    #[test]
495    fn nothing_is_hot_in_a_function_that_never_runs() {
496        // Not an arithmetic edge case. A function whose entry frequency is zero is one the caller
497        // has already decided is unreachable, and every block in it being hot because zero is a
498        // thousandth of zero would put all of it in the hot section.
499        assert!(!Frequency::ENTRY.is_hot_in_function(Frequency::NEVER));
500    }
501
502    #[test]
503    fn hot_in_the_program_says_it_does_not_know_even_about_a_measured_frequency() {
504        // Deliberate, and it stays that way until there is whole program data. The trap this
505        // guards is somebody answering the question from the only number in reach, which is the
506        // frequency within the function, and quietly making the two predicates the same one.
507        assert_eq!(Frequency::ENTRY.is_hot_in_program(), Hotness::Unknown);
508        assert_eq!(Frequency::times(1000, Quality::Precise).is_hot_in_program(), Hotness::Unknown);
509    }
510
511    #[test]
512    fn what_a_dump_shows() {
513        assert_eq!(Probability::percent(73, Quality::Guessed).to_string(), "73%");
514        assert_eq!(Probability::new(7345, Quality::Guessed).to_string(), "73.45%");
515        assert_eq!(Probability::always().to_string(), "100%");
516        assert_eq!(Frequency::ENTRY.to_string(), "1.00 (precise)");
517        assert_eq!(Frequency::UNKNOWN.to_string(), "0.00 (unknown)");
518        assert_eq!(Frequency::times(12, Quality::Guessed).to_string(), "12.00 (guessed)");
519        assert_eq!(Frequency::MAX.to_string(), "saturated (guessed)");
520    }
521}