Skip to main content

rucc_opt/
expect.rs

1//! What `__builtin_expect` said, moved onto the branch it was said about.
2//!
3//! Design: `spec/optimizer/11-profile.md` section 11.2, and tamnd/rucc#364.
4//!
5//! ```c
6//! if (__builtin_expect(error, 0)) { report(); }
7//! ```
8//!
9//! The front end builds an `expect` instruction holding the value and what the program says it will
10//! be. The value is what the branch is on, and this pass is what turns the pair into a statement
11//! about the branch: the arm the hint names gets ninety parts in a hundred, the other gets ten, and
12//! the instruction comes out. Everything downstream reads the number off the arm rather than
13//! chasing the condition back to a node, which is what makes a profile and a hint the same thing to
14//! everything that consumes either.
15//!
16//! # Why it runs first and at every level
17//!
18//! An `expect` sits on the branch condition, and a wrapper on a branch condition is a wrapper on
19//! whatever the peephole, the folder and the comparison simplifier were about to match. Left
20//! standing through a pipeline it would cost code quality on exactly the programs that took the
21//! trouble to say which way their branches go, which is the wrong way round. So it comes off in the
22//! first pass, before anything has had a chance to fail to match through it.
23//!
24//! At `-O0` too, where nothing else runs, for the same reason a stray node is a cost there as well:
25//! `-O0` emits what it is given, and what it is given would otherwise have an instruction in it per
26//! `__builtin_expect` the program wrote. gcc has no such instruction at any level.
27//!
28//! # What it does not do
29//!
30//! It does not predict anything. `crate::predict` is where the prediction is, this only records
31//! what the program claimed, and the difference matters: a hint is a fact about what somebody wrote
32//! and a prediction is a guess, so the ten static predictors write no hints at all. A hint that a
33//! heuristic could have written would be indistinguishable afterwards from one the program did.
34//!
35//! It does not touch a branch whose condition it cannot follow back to an `expect`, and it removes
36//! the instruction either way. A hint nothing can place is a hint nothing can use, and leaving the
37//! instruction standing for a later pass to find would be leaving the wrapper in the way of
38//! everything, which is the thing this exists to avoid.
39
40use std::collections::HashMap;
41
42use rucc_cost::heuristics::PREDICT_EXPECT;
43use rucc_ir::{Block, Def, Extra, Func, Hint, Inst, IntPred, Opcode, Value};
44
45use crate::fold::constant;
46use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats, uses};
47
48/// A branch now says which way the program expects it to go.
49const PLACED: &str = "branch weight written from a __builtin_expect on its condition";
50
51/// The value was expected and nothing branches on it.
52const NO_BRANCH: &str = "__builtin_expect dropped, no branch in this function is on its value";
53
54/// Ran out.
55const NO_FUEL: &str = "__builtin_expect kept, the pass ran out of fuel";
56
57/// The pass.
58#[derive(Debug)]
59pub struct Expect;
60
61impl Pass for Expect {
62    fn name(&self) -> &'static str {
63        "expect"
64    }
65
66    fn describe(&self) -> &'static str {
67        "what __builtin_expect said moves onto the arms of the branch it was said about"
68    }
69
70    fn preserves(&self) -> Preserved {
71        // No block moves and no edge moves, so the graph and everything built on it stand. What
72        // changes is who reads which value, which liveness is a statement about, and what the
73        // branches say about themselves, which is what the frequencies are worked out from.
74        Preserved::ALL.without(Analysis::Liveness).without(Analysis::Frequencies)
75    }
76
77    fn required(&self) -> bool {
78        // This is the only thing that removes an `Opcode::Expect`, and the back end has no rule
79        // that lowers one, so `-fno-expect` would not be a compile without the hints. It would be
80        // a compile that stops on a construct the program never wrote.
81        true
82    }
83
84    fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
85        let mut stats = Stats::new();
86        let mut hints: Vec<Inst> = Vec::new();
87        for block in func.blocks().collect::<Vec<Block>>() {
88            for inst in func.insts(block) {
89                if func[inst].opcode == Opcode::Expect {
90                    hints.push(inst);
91                }
92            }
93        }
94        // The whole of almost every function, since almost no program says anything about its
95        // branches, and the walk above is the only cost this pass has on one that does not.
96        if hints.is_empty() {
97            return stats;
98        }
99
100        let mut placed = 0;
101        for block in func.blocks().collect::<Vec<Block>>() {
102            let Some(term) = func.terminator(block) else { continue };
103            if func[term].opcode != Opcode::BrIf {
104                continue;
105            }
106            let Some(&cond) = func[func[term].args].first() else { continue };
107            let Some((inst, sense)) = through(func, cond) else { continue };
108            let Some(parts) = claim(func, inst, sense) else { continue };
109            if !fuel.take() {
110                stats.missed(NO_FUEL);
111                break;
112            }
113            write(func, term, parts);
114            stats.optimized(PLACED);
115            placed += 1;
116        }
117
118        // Every one of them, and not only the ones a branch was found for. The instruction has done
119        // whatever it is going to do by this point, and what it would do from here is sit in front
120        // of the folder.
121        let mut forward: HashMap<Value, Value> = HashMap::new();
122        for &inst in &hints {
123            let args = &func[func[inst].args];
124            let (Some(&result), Some(&value)) = (func[inst].first_result.as_ref(), args.first())
125            else {
126                continue;
127            };
128            forward.insert(result, value);
129        }
130        uses::substitute(func, &forward);
131        for &inst in &hints {
132            func.remove_inst(inst);
133        }
134        for _ in placed..hints.len() {
135            stats.note(NO_BRANCH);
136        }
137        stats
138    }
139}
140
141/// The `expect` a branch condition comes from, and whether the condition is the expectation being
142/// met or its opposite.
143///
144/// A condition is one bit and an `expect` is as wide as the `long` the prototype converted its
145/// arguments to, so there is always something in between. What the lowering walk writes is a
146/// comparison against zero, and what the peephole writes for `!x` is the same comparison the other
147/// way round, so the chain is comparisons against zero and widenings that do not change whether a
148/// value is zero.
149fn through(func: &Func, cond: Value) -> Option<(Inst, bool)> {
150    let mut value = cond;
151    let mut sense = true;
152    // Bounded, because a chain this walks is a chain of instructions the function has and each step
153    // moves to the operand of the one before it.
154    loop {
155        let Def::Result { inst, .. } = func[value].def else { return None };
156        let data = &func[inst];
157        match data.opcode {
158            Opcode::Expect => return Some((inst, sense)),
159            // A widening keeps a value zero and keeps it non-zero, whichever bit pattern it makes.
160            Opcode::ZExt | Opcode::SExt => value = *func[data.args].first()?,
161            Opcode::ICmp => {
162                let Extra::IntPred(pred) = data.extra else { return None };
163                let args = &func[data.args];
164                let lhs = *args.first()?;
165                let rhs = *args.get(1)?;
166                if literal(func, rhs)? != 0 {
167                    return None;
168                }
169                match pred {
170                    IntPred::Ne => {}
171                    IntPred::Eq => sense = !sense,
172                    _ => return None,
173                }
174                value = lhs;
175            }
176            _ => return None,
177        }
178    }
179}
180
181/// How often the first arm of the branch is taken, in parts of [`Hint::SCALE`], given what the
182/// `expect` says and which way round the condition is.
183///
184/// The probability is the program's where it wrote one and ninety percent otherwise, which is
185/// GCC's `param_builtin_expect_probability` and is section 11.2's number. It is the probability
186/// that the value turns out to be the hint, so a hint of zero is the same claim about the other
187/// arm and the complement is what the branch gets.
188fn claim(func: &Func, inst: Inst, sense: bool) -> Option<u32> {
189    let args = &func[func[inst].args];
190    let value = literal(func, *args.get(1)?)?;
191    let parts = match args.get(2) {
192        Some(&given) => u32::try_from(literal(func, given)?).ok()?.min(Hint::SCALE),
193        None => PREDICT_EXPECT * Hint::SCALE / 100,
194    };
195    let met = (value != 0) == sense;
196    Some(if met { parts } else { Hint::SCALE - parts })
197}
198
199/// The constant a value is, looking through the widenings a converted argument arrives behind.
200///
201/// `__builtin_expect(x, 0)` writes its second argument as an `int` and the prototype converts it to
202/// `long`, so what the lowering walk leaves is a sign extension of a constant and not a constant.
203/// This pass runs before anything that would fold one away, which is the point of it running first,
204/// so the walk is here rather than left to the folder.
205///
206/// A widening only, because it is the one conversion whose answer is the value it was given. A
207/// truncation is not: it can turn a value that is not zero into one that is, which would be the pass
208/// reading a hint the program did not write.
209fn literal(func: &Func, value: Value) -> Option<i128> {
210    let mut value = value;
211    loop {
212        if let Some((bits, ty)) = constant(func, value) {
213            return Some(bits.signed(ty));
214        }
215        let Def::Result { inst, .. } = func[value].def else { return None };
216        let data = &func[inst];
217        match data.opcode {
218            Opcode::ZExt | Opcode::SExt => value = *func[data.args].first()?,
219            _ => return None,
220        }
221    }
222}
223
224/// Writes the claim onto the two arms of a branch, so that the pair sums to certainty.
225fn write(func: &mut Func, term: Inst, parts: u32) {
226    let hint = Hint::parts(parts);
227    for (at, hint) in func.target_list(term).iter().zip([hint, hint.complement()]) {
228        let call = func[at];
229        func.set_block_call(at, rucc_ir::BlockCall { hint, ..call });
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use rucc_base::Interner;
236    use rucc_ir::{Builder, InstData, Signature, Type};
237
238    use super::*;
239
240    /// A function with `blocks` empty blocks in it and nothing else.
241    fn blank(blocks: usize) -> (Interner, Func, Vec<Block>) {
242        let mut names = Interner::new();
243        let mut func = Func::new(names.intern("f"), Signature::new());
244        let list = (0..blocks).map(|_| func.create_block()).collect();
245        (names, func, list)
246    }
247
248    /// A function whose entry branches on `__builtin_expect(x, hint)`, with `x` a parameter.
249    ///
250    /// `parts` is the third operand where there is one, which is what
251    /// `__builtin_expect_with_probability` wrote.
252    fn shaped(hint: i128, parts: Option<i128>) -> (Interner, Func, Vec<Block>) {
253        let (names, mut func, at) = blank(3);
254        let i64_ = Type::int(64);
255        let value = func.append_param(at[0], i64_);
256        let mut build = Builder::new(&mut func, at[0]);
257        let hint = build.iconst(i64_, hint);
258        let mut operands = vec![value, hint];
259        if let Some(parts) = parts {
260            let parts = build.iconst(i64_, parts);
261            operands.push(parts);
262        }
263        let args = build.func().push_values(&operands);
264        let wrapped = build.value(InstData { args, ..InstData::new(Opcode::Expect) }, i64_);
265        let zero = build.iconst(i64_, 0);
266        let cond = build.icmp(IntPred::Ne, wrapped, zero);
267        build.br_if(cond, at[1], &[], at[2], &[]);
268        for block in [at[1], at[2]] {
269            let mut build = Builder::new(&mut func, block);
270            let answer = build.iconst(Type::int(32), 0);
271            build.ret(&[answer]);
272        }
273        (names, func, at)
274    }
275
276    /// What the two arms of the entry's branch say about themselves.
277    fn arms(func: &Func, block: Block) -> Vec<Option<u32>> {
278        let term = func.terminator(block).expect("a branch");
279        func.target_list(term).iter().map(|at| func[at].hint.taken()).collect()
280    }
281
282    /// Runs the pass over a function, and says what it did.
283    fn run(func: &mut Func) -> Stats {
284        Expect.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
285    }
286
287    #[test]
288    fn a_hint_of_one_names_the_arm_taken_when_the_condition_holds() {
289        let (_, mut func, at) = shaped(1, None);
290        run(&mut func);
291        assert_eq!(arms(&func, at[0]), [Some(9_000), Some(1_000)]);
292    }
293
294    #[test]
295    fn a_hint_of_zero_names_the_other_arm() {
296        let (_, mut func, at) = shaped(0, None);
297        run(&mut func);
298        assert_eq!(arms(&func, at[0]), [Some(1_000), Some(9_000)]);
299    }
300
301    /// The shape every program has, since `__builtin_expect(x, 0)` writes an `int` where the
302    /// prototype asks for a `long` and the conversion is an instruction of its own at this point.
303    #[test]
304    fn a_hint_behind_the_conversion_the_prototype_asked_for_is_still_a_hint() {
305        let (_, mut func, at) = blank(3);
306        let i64_ = Type::int(64);
307        let value = func.append_param(at[0], i64_);
308        let mut build = Builder::new(&mut func, at[0]);
309        let narrow = build.iconst(Type::int(32), 0);
310        let hint = build.unary(Opcode::SExt, narrow, i64_);
311        let args = build.func().push_values(&[value, hint]);
312        let wrapped = build.value(InstData { args, ..InstData::new(Opcode::Expect) }, i64_);
313        let zero = build.iconst(i64_, 0);
314        let cond = build.icmp(IntPred::Ne, wrapped, zero);
315        build.br_if(cond, at[1], &[], at[2], &[]);
316        for block in [at[1], at[2]] {
317            let mut build = Builder::new(&mut func, block);
318            let answer = build.iconst(Type::int(32), 0);
319            build.ret(&[answer]);
320        }
321        run(&mut func);
322        assert_eq!(arms(&func, at[0]), [Some(1_000), Some(9_000)]);
323    }
324
325    #[test]
326    fn a_probability_the_program_wrote_is_the_one_the_branch_gets() {
327        let (_, mut func, at) = shaped(1, Some(7_500));
328        run(&mut func);
329        assert_eq!(arms(&func, at[0]), [Some(7_500), Some(2_500)]);
330    }
331
332    /// The one with three operands says how often the value is the hint, so a hint of zero and a
333    /// probability of three quarters is three quarters for the arm taken when it is zero.
334    #[test]
335    fn a_probability_with_a_hint_of_zero_is_about_the_other_arm() {
336        let (_, mut func, at) = shaped(0, Some(7_500));
337        run(&mut func);
338        assert_eq!(arms(&func, at[0]), [Some(2_500), Some(7_500)]);
339    }
340
341    #[test]
342    fn the_instruction_goes_and_its_readers_read_what_it_was_given() {
343        let (_, mut func, at) = shaped(1, None);
344        run(&mut func);
345        let left: Vec<Inst> =
346            func.blocks().flat_map(|block| func.insts(block)).filter(is_expect(&func)).collect();
347        assert!(left.is_empty(), "the wrapper is gone");
348        // The comparison now reads the parameter itself, which is what the wrapper answered with.
349        let term = func.terminator(at[0]).expect("a branch");
350        let cond = *func[func[term].args].first().expect("a condition");
351        let Def::Result { inst, .. } = func[cond].def else { panic!("a comparison") };
352        let read = *func[func[inst].args].first().expect("a left hand side");
353        assert!(matches!(func[read].def, Def::Param { .. }), "it reads the parameter");
354    }
355
356    /// Whether an instruction is the wrapper, as a closure so that the filter above reads.
357    fn is_expect(func: &Func) -> impl Fn(&Inst) -> bool + use<'_> {
358        move |&inst| func[inst].opcode == Opcode::Expect
359    }
360
361    #[test]
362    fn a_function_with_no_hint_in_it_is_left_alone() {
363        let (_, mut func, at) = blank(3);
364        let i64_ = Type::int(64);
365        let value = func.append_param(at[0], i64_);
366        let mut build = Builder::new(&mut func, at[0]);
367        let zero = build.iconst(i64_, 0);
368        let cond = build.icmp(IntPred::Ne, value, zero);
369        build.br_if(cond, at[1], &[], at[2], &[]);
370        for block in [at[1], at[2]] {
371            let mut build = Builder::new(&mut func, block);
372            let answer = build.iconst(Type::int(32), 0);
373            build.ret(&[answer]);
374        }
375
376        let stats = run(&mut func);
377        assert!(!stats.changed(), "nothing to do");
378        assert_eq!(arms(&func, at[0]), [None, None]);
379    }
380
381    /// A condition written the other way round is the same claim about the other arm, which is
382    /// what the sense in [`through`] is for.
383    #[test]
384    fn a_condition_that_is_a_comparison_against_zero_the_other_way_flips_the_arms() {
385        let (_, mut func, at) = blank(3);
386        let i64_ = Type::int(64);
387        let value = func.append_param(at[0], i64_);
388        let mut build = Builder::new(&mut func, at[0]);
389        let hint = build.iconst(i64_, 1);
390        let args = build.func().push_values(&[value, hint]);
391        let wrapped = build.value(InstData { args, ..InstData::new(Opcode::Expect) }, i64_);
392        let zero = build.iconst(i64_, 0);
393        let cond = build.icmp(IntPred::Eq, wrapped, zero);
394        build.br_if(cond, at[1], &[], at[2], &[]);
395        for block in [at[1], at[2]] {
396            let mut build = Builder::new(&mut func, block);
397            let answer = build.iconst(Type::int(32), 0);
398            build.ret(&[answer]);
399        }
400
401        run(&mut func);
402        assert_eq!(arms(&func, at[0]), [Some(1_000), Some(9_000)]);
403    }
404
405    /// A hint about a value nothing branches on is dropped, and the instruction still goes.
406    #[test]
407    fn a_hint_on_a_value_no_branch_reads_leaves_nothing_behind() {
408        let (_, mut func, at) = blank(1);
409        let i64_ = Type::int(64);
410        let value = func.append_param(at[0], i64_);
411        let mut build = Builder::new(&mut func, at[0]);
412        let hint = build.iconst(i64_, 1);
413        let args = build.func().push_values(&[value, hint]);
414        let wrapped = build.value(InstData { args, ..InstData::new(Opcode::Expect) }, i64_);
415        build.ret(&[wrapped]);
416
417        run(&mut func);
418        let left: Vec<Inst> =
419            func.blocks().flat_map(|block| func.insts(block)).filter(is_expect(&func)).collect();
420        assert!(left.is_empty(), "the wrapper is gone");
421        let term = func.terminator(at[0]).expect("a return");
422        let answer = *func[func[term].args].first().expect("a returned value");
423        assert!(matches!(func[answer].def, Def::Param { .. }), "it returns the parameter");
424    }
425}