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