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}