Skip to main content

rucc_opt/
frequency.rs

1//! Block frequency: how often a block runs, relative to the function it is in.
2//!
3//! Design: sections 11.3, 11.4 and 11.5 of `spec/optimizer/11-profile-and-frequency.md`.
4//!
5//! # From probabilities to frequencies
6//!
7//! [`crate::predict`] answers a local question, which is which way one branch goes. Almost every
8//! consumer wants the global one instead: inlining, unrolling, block layout, spill placement and
9//! if-conversion all ask how often this block runs compared with the function entry, and if that
10//! answer is wrong they are all wrong together in a way that is very hard to attribute to anything.
11//!
12//! The frequency of a block is the sum, over the edges into it, of the source's frequency times the
13//! probability of the edge, with the entry pinned at one. On an acyclic graph that is one pass in
14//! reverse postorder. On a loop it is not, because the header's frequency depends on the latch's
15//! and the latch's depends on the header's.
16//!
17//! Wu and Larus's answer, which is the one section 11.3 asks for, is to take the loops from the
18//! inside out. For each loop, work out the cyclic probability, which is how likely the loop is to
19//! go round again, and then the header runs `1 / (1 - p)` times for every entry to it, that being
20//! the sum of the geometric series. The rest of the loop follows from the header by the acyclic
21//! rule. So the whole computation is one walk of the loop forest to get a number per loop and then
22//! one reverse-postorder walk of the function with the back edges left out, and [`Frequency`] does
23//! the series and the clamp in [`Frequency::repeated_while`].
24//!
25//! # The two ways this breaks, and what is done about them
26//!
27//! A loop whose exit no predictor recognised has a cyclic probability of certainty, and one over
28//! zero is not a frequency. The count is capped at [`MAX_PREDICTED_ITERATIONS`], which is section
29//! 11.2's `max-predicted-iterations`, and the cap lives inside the type so that no caller can skip
30//! it. A capped header is recorded, because it is the one place where the sum of what arrives does
31//! not equal what is there, and the check in [`Frequencies::problems`] would otherwise report the
32//! cap as a bug.
33//!
34//! Nested loops multiply, so frequencies overflow. That is section 11.6's first entry, and the
35//! defence is that [`Frequency`] saturates rather than wrapping and says it has.
36//!
37//! # Irreducible regions
38//!
39//! A region with two entries has no header, so there is no series to sum and no well defined
40//! frequency for anything in it. Document 06.4 declines to transform these and this is where the
41//! consequence lands: the blocks get a frequency computed as though the edges that go backwards in
42//! reverse postorder were not there, which is wrong but bounded, and they are marked so a consumer
43//! can decline them. GCC does the same. [`Frequencies::is_reliable`] is the mark, and it spreads
44//! forward, because a block whose frequency was computed from a wrong one is wrong too.
45
46use rucc_cost::heuristics::{MAX_PREDICTED_ITERATIONS, PROFILE_SUM_TOLERANCE_PERCENT};
47use rucc_ir::{Block, Func};
48
49use crate::cfg::Cfg;
50use crate::loops::{LoopId, Loops};
51use crate::predict::{Callees, Predictions};
52use crate::profile::{Frequency, Probability, Quality};
53
54/// How often every block in a function runs, with the entry at one.
55///
56/// The predictions this was worked out from are kept, because every consumer of a frequency wants
57/// the edge probabilities as well and because the two have to be the same pair of numbers or the
58/// check in [`Frequencies::problems`] is checking one against something else.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Frequencies {
61    told: Predictions,
62    of: Vec<Frequency>,
63    reliable: Vec<bool>,
64    capped: Vec<bool>,
65    cyclic: Vec<Probability>,
66    entry: Frequency,
67}
68
69impl Frequencies {
70    /// Predicts every branch and then works out every block's frequency from that.
71    ///
72    /// One walk of the loop forest and one reverse-postorder walk of the function, which is what
73    /// section 11.7 says this costs. A function with no entry, which is a declaration, gets an
74    /// empty answer rather than an error, because a pipeline is handed declarations.
75    #[must_use]
76    pub fn of(func: &Func, cfg: &Cfg, loops: &Loops, callees: &Callees) -> Self {
77        let told = Predictions::of(func, cfg, loops, callees);
78        let width = cfg.capacity();
79        let cyclic = cyclic_probabilities(cfg, loops, &told);
80        let mut of = vec![Frequency::NEVER; width];
81        let mut reliable = vec![true; width];
82        let mut capped = vec![false; width];
83
84        let Some(entry) = cfg.entry() else {
85            return Self { told, of, reliable, capped, cyclic, entry: Frequency::UNKNOWN };
86        };
87        of[entry.index()] = Frequency::ENTRY;
88
89        for block in cfg.reverse_postorder() {
90            if block != entry {
91                let mut total = Frequency::NEVER;
92                let mut sound = !loops.is_irreducible(block);
93                for &pred in cfg.predecessors(block) {
94                    if !forward(cfg, pred, block) {
95                        continue;
96                    }
97                    total = total.plus(of[pred.index()].along(edge(&told, cfg, pred, block)));
98                    sound = sound && reliable[pred.index()];
99                }
100                of[block.index()] = total;
101                reliable[block.index()] = sound;
102            }
103            let Some(id) = heads(loops, block) else { continue };
104            let again = cyclic[id.index()];
105            of[block.index()] = of[block.index()].repeated_while(again, MAX_PREDICTED_ITERATIONS);
106            capped[block.index()] = is_capped(again);
107        }
108
109        let entry = of[entry.index()];
110        Self { told, of, reliable, capped, cyclic, entry }
111    }
112
113    /// The predictions the frequencies were worked out from.
114    #[must_use]
115    pub fn told(&self) -> &Predictions {
116        &self.told
117    }
118
119    /// How likely this edge out of this block is to be the one taken.
120    ///
121    /// The index is into [`Cfg::successors`], which is the order [`Predictions::edges`] is in.
122    #[must_use]
123    pub fn taken(&self, block: Block, index: usize) -> Probability {
124        self.told.taken(block, index)
125    }
126
127    /// How often this block runs, with the entry at one.
128    #[must_use]
129    pub fn get(&self, block: Block) -> Frequency {
130        self.of.get(block.index()).copied().unwrap_or(Frequency::UNKNOWN)
131    }
132
133    /// The entry's own frequency, which is what every other one is relative to.
134    #[must_use]
135    pub fn entry(&self) -> Frequency {
136        self.entry
137    }
138
139    /// Whether this block's frequency means anything.
140    ///
141    /// False inside an irreducible region and anywhere downstream of one. A consumer that cares
142    /// about being right rather than fast should decline these rather than treat them as cold,
143    /// which is what they will look like.
144    #[must_use]
145    pub fn is_reliable(&self, block: Block) -> bool {
146        self.reliable.get(block.index()).copied().unwrap_or(false)
147    }
148
149    /// Whether this block is a loop header whose iteration count hit the cap.
150    ///
151    /// Which is a loop nothing predicted an exit for, so the answer is
152    /// [`MAX_PREDICTED_ITERATIONS`] rather than a number anybody worked out.
153    #[must_use]
154    pub fn is_capped(&self, block: Block) -> bool {
155        self.capped.get(block.index()).copied().unwrap_or(false)
156    }
157
158    /// Whether this block is hot compared with the rest of its function, per section 11.4.
159    #[must_use]
160    pub fn is_hot(&self, block: Block) -> bool {
161        self.get(block).is_hot_in_function(self.entry)
162    }
163
164    /// How likely this loop is to go round again.
165    #[must_use]
166    pub fn cyclic(&self, id: LoopId) -> Probability {
167        self.cyclic.get(id.index()).copied().unwrap_or_else(Probability::never)
168    }
169
170    /// How many times this loop is estimated to run, which is one over the chance of leaving.
171    ///
172    /// Capped at [`MAX_PREDICTED_ITERATIONS`], and that cap is what a loop nothing predicted an
173    /// exit for gets. This is the estimate unrolling and loop alignment want, and it is a guess
174    /// unless it says otherwise.
175    #[must_use]
176    pub fn iterations(&self, id: LoopId) -> u32 {
177        let once = Frequency::ENTRY.repeated_while(self.cyclic(id), MAX_PREDICTED_ITERATIONS);
178        let count = once.raw() / u64::from(Probability::SCALE);
179        u32::try_from(count).unwrap_or(MAX_PREDICTED_ITERATIONS)
180    }
181
182    /// The block that runs most often, and `None` for a function with no blocks.
183    #[must_use]
184    pub fn hottest(&self, func: &Func) -> Option<Block> {
185        func.blocks().max_by_key(|&block| self.get(block).raw())
186    }
187
188    /// What section 11.5 asks the verifier to check after every pass.
189    ///
190    /// Two things. The probabilities out of a block sum to one, and the frequencies arriving at a
191    /// block sum to the block's own frequency. The second is exact in real arithmetic even at a
192    /// loop header, where the entry and the back edge add up to the header precisely because the
193    /// series says they do, so a tolerance of [`PROFILE_SUM_TOLERANCE_PERCENT`] is there for the
194    /// remainder every fixed point division throws away and for nothing else.
195    ///
196    /// Three kinds of block are not checked, and each of them is a place where the sum is known
197    /// not to hold: the entry, which nothing arrives at; a header whose count was capped, where
198    /// the cap is deliberately not the sum; and anything in or downstream of an irreducible
199    /// region, where the frequency was never claimed to mean anything.
200    ///
201    /// What this catches is the pass that split a block and forgot to split its count, which
202    /// section 11.6 says is the failure that costs the most and shows up the least. What it does
203    /// not catch is a proportionally wrong but consistent assignment, and nothing does except
204    /// review.
205    #[must_use]
206    pub fn problems(&self, func: &Func, cfg: &Cfg) -> Vec<String> {
207        let mut problems = Vec::new();
208        let entry = cfg.entry();
209        for block in func.blocks() {
210            let out: u32 = self.told.edges(block).iter().map(|edge| edge.parts()).sum();
211            if !self.told.edges(block).is_empty() && out != Probability::SCALE {
212                problems.push(format!(
213                    "the edges out of {block:?} are taken {out} parts in {} of the time",
214                    Probability::SCALE
215                ));
216            }
217            if Some(block) == entry || self.is_capped(block) || !self.is_reliable(block) {
218                continue;
219            }
220            let mut arriving = Frequency::NEVER;
221            let mut edges = 0;
222            for &pred in cfg.predecessors(block) {
223                arriving = arriving.plus(self.get(pred).along(edge(&self.told, cfg, pred, block)));
224                edges += 1;
225            }
226            let here = self.get(block);
227            let apart = here.raw().abs_diff(arriving.raw());
228            // A percent of the block's own frequency, and on top of that one part for each edge,
229            // because each edge divides by the scale once and loses the remainder.
230            let allowed = here.raw() / 100 * u64::from(PROFILE_SUM_TOLERANCE_PERCENT) + edges;
231            if apart > allowed {
232                problems.push(format!(
233                    "{block:?} runs at {here} and the paths into it add up to {arriving}"
234                ));
235            }
236        }
237        problems
238    }
239}
240
241/// How likely each loop is to go round again.
242///
243/// Innermost first, because an outer loop's cyclic probability is worked out from frequencies that
244/// already have the inner loops' iteration counts in them. [`Loops::all`] is outer before inner, so
245/// this walks it backwards.
246fn cyclic_probabilities(cfg: &Cfg, loops: &Loops, told: &Predictions) -> Vec<Probability> {
247    let mut cyclic = vec![Probability::never(); loops.count()];
248    let mut relative = vec![Frequency::NEVER; cfg.capacity()];
249    let order: Vec<LoopId> = loops.all().collect();
250
251    for &id in order.iter().rev() {
252        let header = loops.header(id);
253        let mut inside: Vec<Block> = loops.blocks(id).to_vec();
254        inside.sort_by_key(|&block| cfg.rank(block));
255        for &block in &inside {
256            relative[block.index()] = Frequency::NEVER;
257        }
258        relative[header.index()] = Frequency::ENTRY;
259
260        for &block in &inside {
261            if block != header {
262                let mut total = Frequency::NEVER;
263                for &pred in cfg.predecessors(block) {
264                    // Everything outside the loop is left out, which for anything but the header
265                    // is nothing: a natural loop is entered at its header and nowhere else.
266                    if !loops.contains(id, pred) || !forward(cfg, pred, block) {
267                        continue;
268                    }
269                    total = total.plus(relative[pred.index()].along(edge(told, cfg, pred, block)));
270                }
271                relative[block.index()] = total;
272            }
273            let Some(inner) = heads(loops, block) else { continue };
274            if inner == id {
275                continue;
276            }
277            let again = cyclic[inner.index()];
278            relative[block.index()] =
279                relative[block.index()].repeated_while(again, MAX_PREDICTED_ITERATIONS);
280        }
281
282        let mut round = Frequency::NEVER;
283        for &latch in loops.latches(id) {
284            round = round.plus(relative[latch.index()].along(edge(told, cfg, latch, header)));
285        }
286        let parts = u32::try_from(round.raw()).unwrap_or(Probability::SCALE);
287        cyclic[id.index()] = Probability::new(parts, round.quality().min(Quality::Guessed));
288    }
289    cyclic
290}
291
292/// The loop this block is the header of, if it heads one.
293fn heads(loops: &Loops, block: Block) -> Option<LoopId> {
294    let id = loops.innermost(block)?;
295    (loops.header(id) == block).then_some(id)
296}
297
298/// Whether this edge goes forwards, which is the edges the acyclic pass may read.
299///
300/// Reverse postorder rank rather than dominance, because the two agree on every back edge of a
301/// natural loop and the rank still answers inside an irreducible region, where there is no header
302/// to dominate anything. An edge from a block the entry never reaches has no rank and is not one.
303fn forward(cfg: &Cfg, from: Block, to: Block) -> bool {
304    match (cfg.rank(from), cfg.rank(to)) {
305        (Some(from), Some(to)) => from < to,
306        _ => false,
307    }
308}
309
310/// How likely this edge is to be the one taken.
311fn edge(told: &Predictions, cfg: &Cfg, from: Block, to: Block) -> Probability {
312    match cfg.successors(from).iter().position(|&block| block == to) {
313        Some(at) => told.taken(from, at),
314        None => Probability::never(),
315    }
316}
317
318/// Whether a loop this likely to go round again is one the cap decided for.
319fn is_capped(again: Probability) -> bool {
320    let stop = Probability::SCALE - again.parts().min(Probability::SCALE);
321    stop <= Probability::SCALE.div_ceil(MAX_PREDICTED_ITERATIONS)
322}
323
324#[cfg(test)]
325mod tests {
326    use rucc_base::Interner;
327    use rucc_ir::{Block, Builder, Func, Signature, Type};
328
329    use super::Frequencies;
330    use crate::cfg::Cfg;
331    use crate::dom::Dominators;
332    use crate::loops::Loops;
333    use crate::predict::Callees;
334    use crate::profile::{Frequency, Probability, Quality};
335
336    /// One entry's worth, which is what every frequency here is a multiple of.
337    const ONE: u64 = Probability::SCALE as u64;
338
339    /// Everything a frequency is worked out from, and then the frequencies.
340    fn frequencies(func: &Func) -> (Frequencies, Cfg, Loops) {
341        let cfg = Cfg::new(func);
342        let doms = Dominators::new(&cfg);
343        let loops = Loops::new(&cfg, &doms);
344        let of = Frequencies::of(func, &cfg, &loops, &Callees::nothing());
345        assert!(of.problems(func, &cfg).is_empty(), "{:?}", of.problems(func, &cfg));
346        (of, cfg, loops)
347    }
348
349    /// A function with `n` blocks.
350    fn blank(blocks: usize) -> (Interner, Func, Vec<Block>) {
351        let mut names = Interner::new();
352        let mut func = Func::new(names.intern("f"), Signature::new());
353        let list = (0..blocks).map(|_| func.create_block()).collect();
354        (names, func, list)
355    }
356
357    /// Returns zero, which is how most of these functions end.
358    fn ret(func: &mut Func, block: Block) {
359        let mut build = Builder::new(func, block);
360        let zero = build.iconst(Type::int(32), 0);
361        build.ret(&[zero]);
362    }
363
364    /// Blocks 0 and 1 in a line, then a return.
365    fn line() -> (Func, Vec<Block>) {
366        let (_, mut func, at) = blank(3);
367        Builder::new(&mut func, at[0]).jump(at[1], &[]);
368        Builder::new(&mut func, at[1]).jump(at[2], &[]);
369        ret(&mut func, at[2]);
370        (func, at)
371    }
372
373    /// A branch nothing predicts, with both arms joining again.
374    fn fork() -> (Func, Vec<Block>) {
375        let (_, mut func, at) = blank(4);
376        let mut build = Builder::new(&mut func, at[0]);
377        let cond = build.iconst(Type::int(1), 1);
378        build.br_if(cond, at[1], &[], at[2], &[]);
379        Builder::new(&mut func, at[1]).jump(at[3], &[]);
380        Builder::new(&mut func, at[2]).jump(at[3], &[]);
381        ret(&mut func, at[3]);
382        (func, at)
383    }
384
385    /// A loop: 0 enters, 1 heads it and tests, 2 is the body and the latch, 3 is after it.
386    fn loop_shape() -> (Func, Vec<Block>) {
387        let (_, mut func, at) = blank(4);
388        Builder::new(&mut func, at[0]).jump(at[1], &[]);
389        let mut build = Builder::new(&mut func, at[1]);
390        let cond = build.iconst(Type::int(1), 1);
391        build.br_if(cond, at[2], &[], at[3], &[]);
392        Builder::new(&mut func, at[2]).jump(at[1], &[]);
393        ret(&mut func, at[3]);
394        (func, at)
395    }
396
397    /// A loop inside a loop: 1 heads the outer, 2 heads the inner, 3 is the inner body, 4 is the
398    /// outer latch, 5 is after both.
399    fn nest() -> (Func, Vec<Block>) {
400        let (_, mut func, at) = blank(6);
401        Builder::new(&mut func, at[0]).jump(at[1], &[]);
402        for (test, stay, leave) in [(at[1], at[2], at[5]), (at[2], at[3], at[4])] {
403            let mut build = Builder::new(&mut func, test);
404            let cond = build.iconst(Type::int(1), 1);
405            build.br_if(cond, stay, &[], leave, &[]);
406        }
407        Builder::new(&mut func, at[3]).jump(at[2], &[]);
408        Builder::new(&mut func, at[4]).jump(at[1], &[]);
409        ret(&mut func, at[5]);
410        (func, at)
411    }
412
413    #[test]
414    fn a_straight_line_runs_once_and_that_is_not_a_guess() {
415        let (func, at) = line();
416        let (of, ..) = frequencies(&func);
417        for block in at {
418            assert_eq!(of.get(block).raw(), ONE, "{block:?}");
419            assert_eq!(of.get(block).quality(), Quality::Precise);
420        }
421    }
422
423    #[test]
424    fn the_arms_of_a_branch_nobody_predicted_run_half_the_time_each() {
425        let (func, at) = fork();
426        let (of, ..) = frequencies(&func);
427        assert_eq!(of.get(at[1]).raw(), ONE / 2);
428        assert_eq!(of.get(at[2]).raw(), ONE / 2);
429        // And the join runs as often as the branch, because the arms put it back together.
430        assert_eq!(of.get(at[3]).raw(), ONE);
431        // Half of a guess is a guess, and so is the sum of two of them.
432        assert_eq!(of.get(at[3]).quality(), Quality::Guessed);
433    }
434
435    #[test]
436    fn a_loop_body_runs_as_many_times_as_the_series_says() {
437        let (func, at) = loop_shape();
438        let (of, _, loops) = frequencies(&func);
439        let id = loops.all().next().expect("a loop");
440        // The back edge is taken 89 times in 100, so the header runs 1 / 0.11 times per entry.
441        assert_eq!(of.cyclic(id), Probability::percent(89, Quality::Guessed));
442        // Which for a loop with one latch and nothing else in it is the header's own edge.
443        assert_eq!(of.taken(at[1], 0), of.cyclic(id));
444        assert_eq!(of.get(at[1]).raw(), ONE * ONE / 1_100);
445        assert_eq!(of.iterations(id), 9);
446        // The body is the header times the chance of staying in.
447        assert_eq!(of.get(at[2]).raw(), of.get(at[1]).along(of.cyclic(id)).raw());
448        // What comes out of a loop that was entered once is a run through it, near enough.
449        assert!(of.get(at[3]).raw().abs_diff(ONE) < ONE / 100, "{}", of.get(at[3]));
450    }
451
452    #[test]
453    fn a_loop_inside_a_loop_multiplies() {
454        let (func, at) = nest();
455        let (of, _, loops) = frequencies(&func);
456        let mut all = loops.all();
457        let outer = all.next().expect("the outer loop");
458        let inner = all.next().expect("the inner loop");
459        assert_eq!(loops.header(outer), at[1]);
460        assert_eq!(loops.header(inner), at[2]);
461        // Nine times round the outer loop and nine round the inner one for each of those.
462        assert_eq!(of.iterations(outer), 9);
463        assert_eq!(of.iterations(inner), 9);
464        let round = u64::from(of.iterations(outer) * of.iterations(inner));
465        assert!(of.get(at[3]).raw() > round * ONE * 3 / 4, "{}", of.get(at[3]));
466        // The block after both runs once, however deep the nest got.
467        assert!(of.get(at[5]).raw().abs_diff(ONE) < ONE / 100, "{}", of.get(at[5]));
468    }
469
470    #[test]
471    fn a_loop_nothing_predicts_an_exit_for_gets_the_cap_rather_than_a_division_by_zero() {
472        let (_, mut func, at) = blank(2);
473        Builder::new(&mut func, at[0]).jump(at[1], &[]);
474        Builder::new(&mut func, at[1]).jump(at[1], &[]);
475        let (of, _, loops) = frequencies(&func);
476        let id = loops.all().next().expect("a loop");
477        assert_eq!(of.cyclic(id).parts(), Probability::SCALE);
478        assert!(of.is_capped(at[1]));
479        assert_eq!(of.iterations(id), 100);
480        assert_eq!(of.get(at[1]).raw(), ONE * 100);
481    }
482
483    #[test]
484    fn a_frequency_in_an_irreducible_region_says_it_does_not_mean_anything() {
485        let (_, mut func, at) = blank(3);
486        let mut build = Builder::new(&mut func, at[0]);
487        let cond = build.iconst(Type::int(1), 1);
488        build.br_if(cond, at[1], &[], at[2], &[]);
489        Builder::new(&mut func, at[1]).jump(at[2], &[]);
490        Builder::new(&mut func, at[2]).jump(at[1], &[]);
491        let (of, ..) = frequencies(&func);
492        assert!(of.is_reliable(at[0]));
493        assert!(!of.is_reliable(at[1]), "a two entry cycle has no header and no series");
494        assert!(!of.is_reliable(at[2]));
495    }
496
497    #[test]
498    fn a_block_nothing_reaches_never_runs_and_is_not_hot() {
499        let (_, mut func, at) = blank(3);
500        Builder::new(&mut func, at[0]).jump(at[1], &[]);
501        ret(&mut func, at[1]);
502        ret(&mut func, at[2]);
503        let (of, ..) = frequencies(&func);
504        assert_eq!(of.get(at[2]), Frequency::NEVER);
505        assert!(!of.is_hot(at[2]));
506        assert!(of.is_hot(at[0]));
507    }
508
509    #[test]
510    fn the_hottest_block_of_a_loop_is_the_one_in_it() {
511        let (func, at) = loop_shape();
512        let (of, ..) = frequencies(&func);
513        assert_eq!(of.hottest(&func), Some(at[1]));
514        assert!(of.is_hot(at[2]));
515        assert_eq!(of.entry(), Frequency::ENTRY);
516    }
517
518    #[test]
519    fn what_arrives_at_a_block_adds_up_to_the_block_which_is_the_check_section_11_5_asks_for() {
520        // `frequencies` runs the check on every shape here, so this one is about it failing.
521        for (func, _) in [line(), fork(), loop_shape(), nest()] {
522            let cfg = Cfg::new(&func);
523            let doms = Dominators::new(&cfg);
524            let loops = Loops::new(&cfg, &doms);
525            let mut of = Frequencies::of(&func, &cfg, &loops, &Callees::nothing());
526            assert!(of.problems(&func, &cfg).is_empty());
527            // A pass that split a block and did not split its count, which is section 11.6's
528            // first failure and the one this check is here for.
529            let last = func.blocks().last().expect("a block");
530            of.of[last.index()] = Frequency::times(7, Quality::Precise);
531            let complaints = of.problems(&func, &cfg);
532            assert_eq!(complaints.len(), 1, "{complaints:?}");
533            assert!(complaints[0].contains("add up to"), "{}", complaints[0]);
534        }
535    }
536}