Skip to main content

rucc_opt/
reload.rs

1//! A load walks back over memory to the store it sees, and takes the value that store wrote.
2//!
3//! Design: `spec/optimizer/16-gvn-and-pre.md` section 16.2. That section asks for two redundant
4//! load eliminators and this is the second of them. The first is [`crate::load`], which keeps one
5//! table per block, compares addresses by identity, and throws everything away at the end of the
6//! block, because what reaches a block from its predecessors is a question it does not ask. This is
7//! the one that asks it.
8//!
9//! # What the difference buys
10//!
11//! A store in a block that dominates the load rather than in the same block, which is every field
12//! read after a loop that wrote it. A load reached through a join where every path agrees about
13//! which store it sees. A load inside a loop whose body writes nothing that could reach it, where
14//! the store is above the loop and the walk cuts the back edge and says so.
15//!
16//! None of those is something the restricted version could be extended to do. They are the walk.
17//!
18//! # The walk
19//!
20//! [`crate::memssa`] is where it lives, and it was written, tested and documented long before
21//! anything called it. What stopped anything calling it was tamnd/rucc#1467: the walk needs an alias
22//! oracle at every step and an oracle wanted the module, and a pass is handed one function. It does
23//! not want the module any more.
24//!
25//! [`Walk::clobber`] answers with five variants rather than two, and the shape of that is what this
26//! pass rests on. [`Clobber::Exact`] is a write that covered exactly the bytes the load reads, and
27//! it is the only one worth acting on. [`Clobber::Partial`] covered some of them, which is a shift
28//! and a truncate away from being the value and is document 16's decision rather than this one's.
29//! [`Clobber::Maybe`] is a write the oracle could not rule out and could not pin down.
30//! [`Clobber::Unknown`] is a walk that ran out of budget or a join whose paths disagreed, and it is
31//! not a no, which is why it has a name rather than being the absence of an answer.
32//!
33//! # Why the store it names dominates the load
34//!
35//! Because the walk only ever gives one instruction back. A join combines the answers from every
36//! path into it and a disagreement is [`Clobber::Unknown`], so an `Exact` naming a store is a store
37//! on every path from the entry to the load, and an instruction on every path to another one
38//! dominates it. That is the whole argument, and it is why this pass does not compute dominance and
39//! does not need to: the value the store wrote dominates the store, the store dominates the load,
40//! and a use put where the load was is a use inside the value's dominance region.
41//!
42//! # The width, which is where the miscompilation would be
43//!
44//! Section 16.6 names a load forwarded from a store of a different size as the most likely wrong
45//! answer in that document, and `Exact` is about bytes rather than about types. Two runs that are
46//! the same bytes can still be two different readings of them, a four byte integer and a four byte
47//! float being the obvious pair, so the types have to be equal as well and a load whose type is not
48//! the stored value's stays and is counted.
49//!
50//! # What is not here
51//!
52//! Phi translation, which is asking about a load whose address is a block parameter in the
53//! predecessor's terms. Section 9.2's `translate`, which is what lets a load be followed through a
54//! `memcpy` and which [`Walk::clobber_with`] already takes a callback for. And a load forwarded
55//! from an earlier load rather than from a store, which the chain does not answer, because a load
56//! is not a def of memory and the walk goes past it: two loads of the same address with the same
57//! version of memory are the same value and saying so is value numbering over memory rather than a
58//! walk. All three are on tamnd/rucc#1476.
59//!
60//! # The chain goes on and comes off again
61//!
62//! [`memssa::build`] before and [`memssa::strip`] after, per function. The back end has never seen
63//! memory SSA and is not going to, and nothing in the pipeline keeps the chain across passes,
64//! because that would mean every edit to the control flow graph anywhere in the optimizer had to
65//! keep the memory parameters in step with the blocks. Two linear walks per function is the price
66//! of not making that claim.
67//!
68//! It has one consequence worth naming. An instruction cannot grow or lose a result, so putting the
69//! chain on and taking it off again replaces every instruction that touches memory with an
70//! equivalent one, even in a function where not a single load was forwarded. The shape of the
71//! function is untouched, so every control flow answer still stands, and the liveness is about
72//! values and does not, which is why this pass drops it whether or not it changed anything.
73
74use std::collections::HashMap;
75
76use rucc_ir::{Block, Flags, Func, Inst, Opcode, Type, Value};
77
78use crate::memssa::{Clobber, Walk};
79use crate::uses::substitute;
80use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats, memssa};
81
82/// What this pass is called, which the pipeline matches on to decide whether to build the module
83/// facts the oracle asks for.
84pub const NAME: &str = "redundant-load";
85
86/// Recorded for a load that took the value of the store the walk said it sees.
87const FORWARDED: &str = "load replaced by the value of the store the walk found";
88
89/// Recorded for a load the walk placed on a write that covered only part of it.
90const PARTIAL: &str = "load kept, what wrote it covers only part of what it reads";
91
92/// Recorded for a load the walk placed on a write it could not pin down.
93const MAYBE: &str = "load kept, something that may have written it could not be pinned down";
94
95/// Recorded for a load whose walk ran out of budget or reached a join whose paths disagreed.
96const UNKNOWN: &str = "load kept, the walk back over memory established nothing";
97
98/// Recorded for a load whose store covered the same bytes at a different type.
99const WIDTH: &str = "load kept, the store that covers it wrote a different type";
100
101/// Recorded for a load the walk placed on a write that is not a store of one value.
102const NOT_A_STORE: &str = "load kept, what covers it writes memory without storing one value";
103
104/// Recorded for a load that would have gone if there had been fuel for it.
105const NO_FUEL: &str = "redundant load kept, the pass ran out of fuel";
106
107/// The pass.
108#[derive(Debug)]
109pub struct RedundantLoad;
110
111impl Pass for RedundantLoad {
112    fn name(&self) -> &'static str {
113        NAME
114    }
115
116    fn describe(&self) -> &'static str {
117        "a load takes the value of the store it sees, wherever in the function that store is"
118    }
119
120    fn preserves(&self) -> Preserved {
121        // The shape of the function, for the reason `crate::load` gives: no block is added, none
122        // is removed, no edge moves, and the instructions that go are loads, which are never
123        // terminators. The memory parameters this puts on the joins come back off before the pass
124        // returns, so no block ends with a parameter it did not start with.
125        Preserved::ALL.without(Analysis::Liveness)
126    }
127
128    fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
129        let mut stats = Stats::new();
130        if !memssa::build(func) {
131            return stats;
132        }
133        // What each removed load's result is read as, applied to the whole function once at the
134        // end, for the reason `crate::load` gives: rewriting each one where it is found would be a
135        // walk over the function per load and there is nothing to gain by it.
136        let mut forward: HashMap<Value, Value> = HashMap::new();
137        let mut gone: Vec<Inst> = Vec::new();
138
139        // The walk borrows the function, so it is a scope of its own and every edit happens after
140        // it. Built once, because the alias oracle inside it holds an escape analysis that is one
141        // walk over the function and every step of every walk may ask it.
142        {
143            let mut walk = Walk::new(func, an.outside());
144            for block in func.blocks().collect::<Vec<Block>>() {
145                for inst in func.insts(block).collect::<Vec<Inst>>() {
146                    let Some((result, ty)) = reads(func, inst) else {
147                        continue;
148                    };
149                    match walk.clobber(inst) {
150                        Clobber::Exact(wrote) => {
151                            let Some(value) = stored(func, wrote) else {
152                                stats.missed(NOT_A_STORE);
153                                continue;
154                            };
155                            if func[value].ty != ty {
156                                stats.missed(WIDTH);
157                                continue;
158                            }
159                            if !fuel.take() {
160                                // Out of fuel is a request to stop transforming and not to stop
161                                // looking, so the walk goes on and the count of what could have
162                                // gone is the same at every setting, which is what makes a
163                                // bisection over it monotonic.
164                                stats.missed(NO_FUEL);
165                                continue;
166                            }
167                            forward.insert(result, value);
168                            gone.push(inst);
169                            stats.optimized(FORWARDED);
170                        }
171                        Clobber::Partial(_) => stats.missed(PARTIAL),
172                        Clobber::Maybe(_) => stats.missed(MAYBE),
173                        Clobber::Unknown => stats.missed(UNKNOWN),
174                        // Nothing in the function wrote it, so the load reads whatever was there
175                        // when the function started. There is no value here to take and nothing
176                        // was missed either, so it is not counted as one.
177                        Clobber::NoClobber => {}
178                    }
179                }
180            }
181            let counts = walk.counts();
182            if counts.walks() > 0 {
183                stats.record(crate::stats::Kind::Note, WALKS, count(counts.walks()));
184                stats.record(crate::stats::Kind::Note, STEPS, count(counts.steps()));
185                if counts.exhausted() > 0 {
186                    stats.record(crate::stats::Kind::Note, EXHAUSTED, count(counts.exhausted()));
187                }
188            }
189        }
190
191        for inst in gone {
192            func.remove_inst(inst);
193        }
194        if !forward.is_empty() {
195            substitute(func, &forward);
196        }
197        memssa::strip(func);
198        // Said here rather than left to `preserves`, because the manager takes a pass that changed
199        // nothing to have preserved everything and this one has not: the chain going on and coming
200        // off gives every instruction that touches memory a new name whatever the pass did with
201        // them.
202        an.settle(func, self.preserves(), false);
203        stats
204    }
205}
206
207/// Recorded as a note: how many walks were made.
208const WALKS: &str = "walks back over memory";
209
210/// Recorded as a note: how many defs those walks looked at, which is one alias query each.
211const STEPS: &str = "memory defs the walks looked at";
212
213/// Recorded as a note: how many walks gave up rather than answering.
214///
215/// Section 9.3 of `spec/optimizer/09-memory-ssa.md` says this number decides whether the walk gets
216/// a cache. Above one percent of walks and the budget is too small or the alias analysis is too
217/// weak, and both of those are better fixed than cached around.
218const EXHAUSTED: &str = "walks that ran out of budget";
219
220/// A count as the record holds them, which is narrower than the counters are.
221fn count(of: u64) -> u32 {
222    u32::try_from(of).unwrap_or(u32::MAX)
223}
224
225/// The result and the type of a load worth asking about, and nothing for anything else.
226///
227/// Narrow on purpose, and the same shape [`crate::load`] uses. A plain non-volatile `Load` with one
228/// address and one result. `AtomicLoad` is a separate opcode in this IR and is not this one, so an
229/// ordering never reaches here as something to forward.
230fn reads(func: &Func, inst: Inst) -> Option<(Value, Type)> {
231    let data = &func[inst];
232    if data.opcode != Opcode::Load || data.flags.contains(Flags::VOLATILE) {
233        return None;
234    }
235    let mut results = data.results();
236    let (Some(result), None) = (results.next(), results.next()) else {
237        return None;
238    };
239    Some((result, func[result].ty))
240}
241
242/// What a store wrote, and nothing for anything else that writes memory.
243///
244/// The walk answers `Exact` for any write that covered exactly the bytes the load reads, and a
245/// `memcpy` or a `memset` can do that without there being one value anywhere to take.
246fn stored(func: &Func, inst: Inst) -> Option<Value> {
247    let data = &func[inst];
248    if data.opcode != Opcode::Store || data.flags.contains(Flags::VOLATILE) {
249        return None;
250    }
251    func[data.args].first().copied()
252}
253
254#[cfg(test)]
255mod tests {
256    use std::sync::Arc;
257
258    use rucc_base::Interner;
259    use rucc_ir::{Module, parse, verify_func};
260
261    use super::*;
262    use crate::outside::Outside;
263
264    const HEADER: &str = "\
265; ModuleID = 'mem.c'
266; format 0
267target triple = \"x86_64-unknown-linux-gnu\"
268target datalayout = \"e-p:64:64-i64:64-f80:128-S128\"
269";
270
271    fn wrap(signature: &str, body: &str) -> String {
272        format!("{HEADER}\nfunc @f{signature}, linkage(external) {{\n{body}}}\n")
273    }
274
275    /// Runs the pass over the one function in the text and insists the result verifies, which is
276    /// where most of the strength of these tests is: the chain goes on and comes off again, and a
277    /// half removed chain is exactly the kind of thing a shape assertion would let through.
278    fn run(text: &str) -> (Module, Stats) {
279        let mut names = Interner::new();
280        let mut module = parse(text, &mut names).expect("the text parses");
281        let id = module.funcs().next().expect("one function");
282        let outside = Arc::new(Outside::of(&module));
283        let mut an = crate::machine::fixtures::analyses().about(outside);
284        let stats = RedundantLoad.run(&mut module[id], &mut an, &mut Fuel::unlimited());
285        if let Err(errors) = verify_func(&module, &module[id], &names) {
286            panic!("{errors:#?}");
287        }
288        (module, stats)
289    }
290
291    fn one(module: &Module) -> &Func {
292        &module[module.funcs().next().expect("one function")]
293    }
294
295    /// How many instructions with that opcode the function has left.
296    fn count_of(func: &Func, opcode: Opcode) -> usize {
297        func.blocks()
298            .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
299            .filter(|&inst| func[inst].opcode == opcode)
300            .count()
301    }
302
303    /// Nothing anywhere in the function is on the chain.
304    fn off(func: &Func) {
305        for block in func.blocks() {
306            assert!(func[block].params.iter().all(|&param| !func[param].ty.is_mem()));
307            for inst in func.insts(block) {
308                assert_ne!(func[inst].opcode, Opcode::MemEntry);
309                assert!(!func.carries_mem(inst));
310            }
311        }
312    }
313
314    #[test]
315    fn a_store_in_a_block_above_the_load_reaches_it() {
316        // The case the one block version was built not to handle, and the reason this pass is
317        // worth its two walks.
318        let text = wrap(
319            "(ptr, i1) -> i32",
320            "block0(%0: ptr, %1: i1):
321    %2 = iconst.i32 7
322    store %2 -> %0, align 4
323    br_if %1, block1, block2
324
325block1:
326    jump block2
327
328block2:
329    %3 = load.i32 %0, align 4
330    return %3
331",
332        );
333        let (module, stats) = run(&text);
334        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
335        let func = one(&module);
336        off(func);
337        assert_eq!(count_of(func, Opcode::Load), 0, "the load is still there");
338        // What the function returns is now the constant the store wrote.
339        let ret = func
340            .blocks()
341            .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
342            .find(|&inst| func[inst].opcode == Opcode::Return)
343            .expect("a return");
344        let returned = func[func[ret].args][0];
345        let seven = func
346            .blocks()
347            .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
348            .find(|&inst| func[inst].opcode == Opcode::IConst)
349            .expect("the constant");
350        assert_eq!(returned, func[seven].results().next().expect("a result"));
351    }
352
353    #[test]
354    fn a_store_down_only_one_arm_is_not_an_answer() {
355        // One path into the join wrote it and the other did not, and a disagreement is `Unknown`
356        // rather than the weaker of the two, because there is no order on these to act on.
357        let text = wrap(
358            "(ptr, i1) -> i32",
359            "block0(%0: ptr, %1: i1):
360    br_if %1, block1, block2
361
362block1:
363    %2 = iconst.i32 7
364    store %2 -> %0, align 4
365    jump block3
366
367block2:
368    jump block3
369
370block3:
371    %3 = load.i32 %0, align 4
372    return %3
373",
374        );
375        let (module, stats) = run(&text);
376        assert!(!stats.changed(), "a store on one path is not the value on both");
377        assert_eq!(stats.count(crate::stats::Kind::Missed, UNKNOWN), 1);
378        off(one(&module));
379    }
380
381    #[test]
382    fn both_arms_storing_the_same_way_is_still_two_stores() {
383        // Two stores of the same value are two instructions, so the paths name two different
384        // clobbers and disagree. Taking this one wants value numbering over the stores rather
385        // than a better walk, and it is worth a test saying which of the two it needs.
386        let text = wrap(
387            "(ptr, i1) -> i32",
388            "block0(%0: ptr, %1: i1):
389    %2 = iconst.i32 7
390    br_if %1, block1, block2
391
392block1:
393    store %2 -> %0, align 4
394    jump block3
395
396block2:
397    store %2 -> %0, align 4
398    jump block3
399
400block3:
401    %3 = load.i32 %0, align 4
402    return %3
403",
404        );
405        let (_, stats) = run(&text);
406        assert!(!stats.changed());
407    }
408
409    #[test]
410    fn a_loop_that_writes_nothing_keeps_the_store_above_it() {
411        // The back edge leads to the parameter the walk started from, which is how a cycle is cut
412        // and contributes nothing, so what is left is the one path that wrote it.
413        let text = wrap(
414            "(ptr, i1) -> i32",
415            "block0(%0: ptr, %1: i1):
416    %2 = iconst.i32 7
417    store %2 -> %0, align 4
418    jump block1
419
420block1:
421    %3 = load.i32 %0, align 4
422    br_if %1, block1, block2
423
424block2:
425    return %3
426",
427        );
428        let (module, stats) = run(&text);
429        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
430        assert_eq!(count_of(one(&module), Opcode::Load), 0);
431    }
432
433    #[test]
434    fn a_store_inside_the_loop_stops_it() {
435        let text = wrap(
436            "(ptr, i1) -> i32",
437            "block0(%0: ptr, %1: i1):
438    %2 = iconst.i32 7
439    store %2 -> %0, align 4
440    jump block1
441
442block1:
443    %3 = load.i32 %0, align 4
444    %4 = add %3, %3
445    store %4 -> %0, align 4
446    br_if %1, block1, block2
447
448block2:
449    return %3
450",
451        );
452        let (_, stats) = run(&text);
453        assert!(!stats.changed(), "the body writes what the load reads");
454    }
455
456    #[test]
457    fn the_bytes_being_the_same_is_not_the_type_being_the_same() {
458        // Section 16.6's most likely wrong answer. Four bytes stored and four bytes read is the
459        // same run of memory and two different readings of it, and this pass takes neither.
460        let text = wrap(
461            "(ptr) -> f32",
462            "block0(%0: ptr):
463    %1 = iconst.i32 7
464    store %1 -> %0, align 4
465    %2 = load.f32 %0, align 4
466    return %2
467",
468        );
469        let (_, stats) = run(&text);
470        assert!(!stats.changed());
471        assert_eq!(stats.count(crate::stats::Kind::Missed, WIDTH), 1);
472    }
473
474    #[test]
475    fn a_volatile_load_has_to_happen() {
476        let text = wrap(
477            "(ptr) -> i32",
478            "block0(%0: ptr):
479    %1 = iconst.i32 7
480    store %1 -> %0, align 4
481    %2 = load.i32.volatile %0, align 4
482    return %2
483",
484        );
485        let (module, stats) = run(&text);
486        assert!(!stats.changed());
487        assert_eq!(count_of(one(&module), Opcode::Load), 1);
488    }
489
490    #[test]
491    fn a_function_with_no_memory_in_it_is_left_alone() {
492        let text = wrap(
493            "(i32) -> i32",
494            "block0(%0: i32):
495    %1 = add %0, %0
496    return %1
497",
498        );
499        let (module, stats) = run(&text);
500        assert!(stats.is_empty(), "there was nothing here to say anything about");
501        off(one(&module));
502    }
503
504    #[test]
505    fn out_of_fuel_keeps_the_load_and_still_counts_it() {
506        // The count of what could have gone is the same at every fuel setting, which is what
507        // makes a bisection over it monotonic.
508        let text = wrap(
509            "(ptr) -> i32",
510            "block0(%0: ptr):
511    %1 = iconst.i32 7
512    store %1 -> %0, align 4
513    %2 = load.i32 %0, align 4
514    return %2
515",
516        );
517        let mut names = Interner::new();
518        let mut module = parse(&text, &mut names).expect("the text parses");
519        let id = module.funcs().next().expect("one function");
520        let outside = Arc::new(Outside::of(&module));
521        let mut an = crate::machine::fixtures::analyses().about(outside);
522        let stats = RedundantLoad.run(&mut module[id], &mut an, &mut Fuel::of(0));
523        assert!(!stats.changed());
524        assert_eq!(stats.count(crate::stats::Kind::Missed, NO_FUEL), 1);
525        off(&module[id]);
526    }
527
528    #[test]
529    fn what_the_walks_cost_is_written_down() {
530        // Section 9.3 asks for the fraction that ran out of budget by name, and a number nothing
531        // reports is a number nobody will look at.
532        let text = wrap(
533            "(ptr) -> i32",
534            "block0(%0: ptr):
535    %1 = iconst.i32 7
536    store %1 -> %0, align 4
537    %2 = load.i32 %0, align 4
538    return %2
539",
540        );
541        let (_, stats) = run(&text);
542        assert_eq!(stats.count(crate::stats::Kind::Note, WALKS), 1);
543        assert_eq!(stats.count(crate::stats::Kind::Note, STEPS), 1);
544        assert_eq!(stats.count(crate::stats::Kind::Note, EXHAUSTED), 0);
545    }
546
547    #[test]
548    fn the_safety_instrumentation_between_them_does_not_stop_the_forward() {
549        // What a safety build looks like by the time the optimizer sees it, which is the shape
550        // above with the lifetime plane written and then read between the store and the load.
551        // Both of those are on the memory chain and both have `%0` as an operand, so the walk
552        // goes through them and has to be told they are not about `%0`.
553        let text = wrap(
554            "(ptr, i1) -> i32",
555            "block0(%0: ptr, %1: i1):
556    %2 = iconst.i32 7
557    store %2 -> %0, align 4
558    %3 = iconst.i64 4
559    meta_init %0, %3
560    br_if %1, block1, block2
561
562block1:
563    %4 = cap_of %0
564    check_bounds %4, %0, size 4, align 4
565    jump block2
566
567block2:
568    %5 = load.i32 %0, align 4
569    return %5
570",
571        );
572        let (module, stats) = run(&text);
573        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
574        let func = one(&module);
575        off(func);
576        assert_eq!(count_of(func, Opcode::Load), 0);
577        // And the instrumentation is still there, because this pass forwards loads and is not
578        // entitled to an opinion about whether a check was worth running.
579        assert_eq!(count_of(func, Opcode::MetaInit), 1);
580        assert_eq!(count_of(func, Opcode::CheckBounds), 1);
581    }
582}