Skip to main content

rucc_codegen/
weights.rs

1//! How often each block runs, carried from the IR down to the machine IR.
2//!
3//! Design: `spec/optimizer/38-scheduling-and-layout.md` sections 38.4 and 38.6, and
4//! tamnd/rucc#364, which is the observation that a branch weight is worked out and then thrown
5//! away because nothing downstream could read one.
6//!
7//! [`rucc_opt::Frequencies`] answers, for an IR function, how often every block runs next to the
8//! once the function is entered, and how likely each arm of each branch is to be the one taken.
9//! Every one of its consumers so far has been a pass in the middle end, and the consumer this is
10//! for is [`crate::layout`], which is at the far end of selection, allocation and the prologue.
11//! None of those could work the numbers out for itself: by then a loop is a backward branch and
12//! the loop forest the frequency was summed over is gone.
13//!
14//! So the numbers are copied onto the blocks and the arms as soon as there are blocks and arms to
15//! copy them onto, which is the moment selection finishes. [`mir::Weight`] is the same scale the
16//! frequency is in, so the copy is a copy.
17//!
18//! # Why it is a pass over what selection left rather than part of selection
19//!
20//! Selection makes one machine block per IR block, in the same order, with the arms in the same
21//! order, and says so by handing back [`crate::lower::Lowered::blocks`]. That correspondence is
22//! the whole of what this needs, and it is a fact worth spending rather than a reason to thread a
23//! second table of numbers through four thousand lines of instruction selection.
24//!
25//! # What is not carried
26//!
27//! Nothing keeps a weight in step with the graph after this. A pass that makes a block says how
28//! often it runs if it knows, and [`crate::split::critical`] does, and everything else leaves the
29//! new block running as often as the function does. That is a heuristic going slightly stale, not
30//! a fact going wrong, and [`mir::Weight`] says so where it is defined.
31
32use rucc_ir as ir;
33use rucc_mir as mir;
34use rucc_opt::{Callees, Cfg, Dominators, Frequencies, Loops};
35
36/// Writes onto a machine function how often each of its blocks runs and each of its arms is
37/// taken, worked out from the IR function it was selected from.
38///
39/// `blocks` is [`crate::lower::Lowered::blocks`], which is the machine block each IR block
40/// became. A function nothing was lowered from is left alone, and so is every machine block that
41/// came from no IR block, which is what a block some later pass added looks like from here.
42pub fn carry(source: &ir::Func, blocks: &[Option<mir::Block>], func: &mut mir::Func) {
43    let cfg = Cfg::new(source);
44    if cfg.entry().is_none() {
45        return;
46    }
47    let doms = Dominators::new(&cfg);
48    let loops = Loops::new(&cfg, &doms);
49    // The call graph's answer to whether a callee comes back is a fact about the module and this
50    // only ever sees the one function, so the predictor is left with the answer the IR gives
51    // directly, which is an `unreachable` after a call. That is where most of it comes from
52    // anyway, since C error handling is a call to `abort` and the front end writes the
53    // `unreachable` behind it.
54    let freqs = Frequencies::of(source, &cfg, &loops, &Callees::nothing());
55    for block in source.blocks() {
56        let Some(&Some(out)) = blocks.get(block.index()) else { continue };
57        let weight = freqs.get(block);
58        func.set_weight(out, mir::Weight::parts(weight.raw()));
59        let Some(term) = source.terminator(block) else { continue };
60        let arms: Vec<ir::Block> = source.successors(term).map(|call| call.block).collect();
61        if arms.len() != func[out].succs.len() {
62            continue;
63        }
64        for (index, arm) in arms.iter().enumerate() {
65            // The probabilities are indexed by the graph's successors and not by the
66            // terminator's arms, and the two differ on a branch whose arms agree and on a switch
67            // with two labels on one case: the graph names such a block once. So the arm is
68            // looked up by where it goes rather than by where it is, and two arms that go to one
69            // block each carry the whole of that block's share. Nothing is lost by that, because
70            // the layout's question is which block to put next and both arms answer the same.
71            let Some(at) = cfg.successors(block).iter().position(|succ| succ == arm) else {
72                continue;
73            };
74            func.succs_mut(out)[index].weight =
75                mir::Weight::parts(weight.along(freqs.taken(block, at)).raw());
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use rucc_base::Interner;
83    use rucc_ir::{Builder, Func, Opcode, Signature, Type};
84    use rucc_target::x86_64::SYSV;
85
86    use super::*;
87    use crate::elsewhere::Elsewhere;
88    use crate::lower;
89
90    /// Lowers a function and carries its weights down, and gives back the machine function.
91    fn lowered(source: &mut Func, names: &mut Interner) -> mir::Func {
92        let out = lower::func(source, names, &SYSV, &Elsewhere::default()).expect("it lowers");
93        let lower::Lowered { mut func, blocks, .. } = out;
94        carry(source, &blocks, &mut func);
95        func
96    }
97
98    /// The weights of every block, in layout order, which at this point is the order they were
99    /// made in.
100    fn weights(func: &mir::Func) -> Vec<u64> {
101        func.blocks().map(|block| func[block].weight.raw()).collect()
102    }
103
104    #[test]
105    fn a_function_with_no_branch_in_it_runs_every_block_once() {
106        let mut names = Interner::new();
107        let mut source = Func::new(names.intern("f"), Signature::new());
108        let entry = source.create_block();
109        Builder::new(&mut source, entry).ret(&[]);
110
111        let func = lowered(&mut source, &mut names);
112
113        assert_eq!(weights(&func), [mir::Weight::ONCE.raw()]);
114    }
115
116    #[test]
117    fn the_arms_of_a_branch_add_up_to_the_block_they_leave() {
118        let int = Type::int(32);
119        let mut names = Interner::new();
120        let mut source =
121            Func::new(names.intern("f"), Signature::new().with_params(&[Type::int(1)]));
122        let entry = source.create_block();
123        let cond = source.append_param(entry, Type::int(1));
124        let yes = source.create_block();
125        let no = source.create_block();
126        Builder::new(&mut source, entry).br_if(cond, yes, &[], no, &[]);
127        let mut build = Builder::new(&mut source, yes);
128        let one = build.iconst(int, 1);
129        build.ret(&[one]);
130        let mut build = Builder::new(&mut source, no);
131        let two = build.iconst(int, 2);
132        build.ret(&[two]);
133
134        let func = lowered(&mut source, &mut names);
135
136        let head = func.blocks().next().expect("an entry");
137        let arms: Vec<u64> = func[head].succs.iter().map(|call| call.weight.raw()).collect();
138        assert_eq!(arms.len(), 2);
139        assert_eq!(arms.iter().sum::<u64>(), mir::Weight::ONCE.raw());
140    }
141
142    #[test]
143    fn a_loop_body_runs_more_often_than_the_block_that_follows_it() {
144        let int = Type::int(32);
145        let mut names = Interner::new();
146        let mut source =
147            Func::new(names.intern("f"), Signature::new().with_params(&[Type::int(1)]));
148        let entry = source.create_block();
149        let cond = source.append_param(entry, Type::int(1));
150        let head = source.create_block();
151        let body = source.create_block();
152        let out = source.create_block();
153        Builder::new(&mut source, entry).jump(head, &[]);
154        Builder::new(&mut source, head).br_if(cond, body, &[], out, &[]);
155        Builder::new(&mut source, body).jump(head, &[]);
156        let mut build = Builder::new(&mut source, out);
157        let zero = build.iconst(int, 0);
158        build.ret(&[zero]);
159
160        let func = lowered(&mut source, &mut names);
161
162        let made: Vec<mir::Block> = func.blocks().collect();
163        let weight = |at: usize| func[made[at]].weight.raw();
164        assert!(weight(2) > weight(3), "the body {} the exit {}", weight(2), weight(3));
165        // The exit runs once per call, give or take the one part in ten thousand the geometric
166        // series loses to integer division on the way round the loop.
167        assert!(weight(3).abs_diff(mir::Weight::ONCE.raw()) <= 1, "the exit {}", weight(3));
168    }
169
170    #[test]
171    fn the_arm_control_does_not_come_back_from_is_the_colder_one() {
172        let int = Type::int(32);
173        let mut names = Interner::new();
174        let mut source =
175            Func::new(names.intern("f"), Signature::new().with_params(&[Type::int(1)]));
176        let entry = source.create_block();
177        let cond = source.append_param(entry, Type::int(1));
178        let yes = source.create_block();
179        let no = source.create_block();
180        Builder::new(&mut source, entry).br_if(cond, yes, &[], no, &[]);
181        // The arm that calls nothing and returns, against the arm control does not come back
182        // from, which is the predictor that needs no source and is the one C error handling
183        // shows up as.
184        Builder::new(&mut source, yes).inst(ir::InstData::new(Opcode::Unreachable), &[]);
185        let mut build = Builder::new(&mut source, no);
186        let zero = build.iconst(int, 0);
187        build.ret(&[zero]);
188
189        let func = lowered(&mut source, &mut names);
190
191        let head = func.blocks().next().expect("an entry");
192        let arms: Vec<u64> = func[head].succs.iter().map(|call| call.weight.raw()).collect();
193        assert!(arms[0] < arms[1], "{arms:?}");
194    }
195}