Skip to main content

rucc_opt/
load.rs

1//! A load of something the block has already read or written is that value, not a second read.
2//!
3//! Design: `spec/optimizer/16-gvn-and-pre.md` section 16.2, which calls redundant load elimination
4//! the real prize of that document. Value numbering over arithmetic is worth less than people
5//! expect on C, because the front end does not generate the same expression twice and the
6//! programmer does not write it twice. Loads are different. `p->x` three times in a function is
7//! three reads of memory, and if nothing wrote through an aliasing pointer in between, two of them
8//! are work the program does not have to do.
9//!
10//! # The restricted version, and why this is it
11//!
12//! Section 16.2 asks for two of these. The one at `-O2` walks memory SSA back from each load to
13//! its clobbering definition, translates an address backwards through a block's parameters, and
14//! sees through a `memcpy`. The one here is the other: same block only, no phi translation, no
15//! `memcpy`, and no memory SSA at all. That is the version the section says should be at `-O1`,
16//! and it says why: it catches the repeated `p->x` in one basic block, which is the majority of the
17//! opportunities, for a fraction of the machinery.
18//!
19//! The two are not alternatives and this is not a stand-in for the other one. What it is is the
20//! part that can be written without an alias oracle, and the part whose cost is one walk over each
21//! block.
22//!
23//! # What it knows
24//!
25//! One table per block, from an address to the value that address holds, thrown away at the end of
26//! the block because what reaches a block from its predecessors is the question this version does
27//! not ask. A store writes what it stored. A load that had to happen writes what it read. Either
28//! way the next load of that address is that value.
29//!
30//! An address is one SSA value, compared by identity. Two pointers that are the same address by
31//! arithmetic and not by name are two addresses here, which costs opportunities and no
32//! correctness: an address computed twice out of the same parts is tracked twice and neither copy
33//! is forwarded to the other. What would give the two one name is value numbering over the
34//! arithmetic, which is the other half of document 16 and is not built. It costs more than it
35//! sounds like it should. `a[i] = v; total += a[i];` written in C is a store and a load whose
36//! addresses are two separate runs of the same multiply and add, because the front end emits the
37//! subscript twice, so the shape this pass is most obviously for is one it cannot see until that
38//! lands.
39//!
40//! # What throws the table away
41//!
42//! Anything that could write anywhere. There is no alias analysis in this pass, so a store to one
43//! address is treated as a possible write to every address, and the table is emptied before the
44//! store records what it just wrote. A call, an atomic, a fence and a `memcpy` empty it and record
45//! nothing. That is [`Opcode::touches_memory`], which is the conservative predicate, so an opcode
46//! added to the IR later throws the table away rather than being quietly assumed harmless.
47//!
48//! This costs less than it sounds like. A store immediately followed by a read of what was stored
49//! still works, because the store empties the table and then puts back the one entry the load is
50//! about to ask for. What the barrier really costs is the second address: `*p = v; total += *q;`
51//! with two locals is refused even where the two cannot be the same object, and telling them apart
52//! is the alias oracle's answer rather than this pass's.
53//!
54//! A volatile access empties the table and records nothing either way. Whether a volatile store
55//! could be forwarded from is an argument about what `volatile` promises, and this pass does not
56//! need to have it.
57//!
58//! # The width, which is where the miscompilation would be
59//!
60//! Section 16.6 names it as the single most likely wrong answer in that document: a load forwarded
61//! from a store of a different size. Section 09.5 has the three-way distinction, which is that a
62//! store covering the load exactly is the value, one covering it partially needs an extract, and
63//! one not covering it at all means the walk should continue.
64//!
65//! This pass only ever takes the first of the three. The address has to be the same SSA value and
66//! the type has to be equal, which is the same width and the same reading of the bits, and
67//! anything else is left alone and counted. Two-way is what somebody writes first and it is right
68//! most of the time, which is what makes it worth being explicit that this is not that.
69//!
70//! # What it leaves behind
71//!
72//! The load goes, rather than staying and having its result forwarded. [`crate::dce`] would not
73//! remove it: `has_effects` is true of every load, because a pass that removed one would need to
74//! know the address is dereferenced anyway, and this is the pass that knows it. The load being
75//! removed is safe for a reason nothing else in the pipeline has: something already read or wrote
76//! that exact address in this block, so the address is one the program dereferences whatever
77//! happens next.
78//!
79//! The store stays. Removing a store that a later store covers is dead store elimination, which is
80//! document 17 and a different pass.
81
82use std::collections::HashMap;
83
84use rucc_ir::{Block, Flags, Func, Inst, Opcode, Type, Value};
85
86use crate::uses::substitute;
87use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
88
89/// Recorded for a load that read what a store in the same block had just written.
90const FORWARDED: &str = "load replaced by the value a store in the same block wrote there";
91
92/// Recorded for a load of an address an earlier load in the same block had already read.
93const REUSED: &str = "load replaced by what an earlier load of the same address read";
94
95/// Recorded for a load whose address is known and whose type is not the type it is known at.
96const WIDTH: &str = "load kept, what is known about that address is a different type";
97
98/// Recorded for a load that would have gone if there had been fuel for it.
99const NO_FUEL: &str = "redundant load kept, the pass ran out of fuel";
100
101/// The pass.
102#[derive(Debug)]
103pub struct LoadForward;
104
105impl Pass for LoadForward {
106    fn name(&self) -> &'static str {
107        "load-forward"
108    }
109
110    fn describe(&self) -> &'static str {
111        "a load of an address the block has already read or written is that value"
112    }
113
114    fn preserves(&self) -> Preserved {
115        // The shape of the function. No block is added, none is removed, no edge moves, and the
116        // instructions that go are loads, which are never terminators.
117        //
118        // The liveness is the one thing that does move, for the reason `crate::simplify` gives:
119        // pointing every reader of one value at another is one more place the second is live and
120        // one fewer the first is.
121        Preserved::ALL.without(Analysis::Liveness)
122    }
123
124    fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
125        let mut stats = Stats::new();
126        // What each removed load's result is read as, applied to the whole function once at the
127        // end. Rewriting each one where it is found would be a walk over the function per load,
128        // and there is nothing to gain by it: what this pass looks at is the address, and a
129        // redirection of a result does not change one.
130        let mut forward: HashMap<Value, Value> = HashMap::new();
131        let mut gone: Vec<Inst> = Vec::new();
132
133        for block in func.blocks().collect::<Vec<Block>>() {
134            let mut known: HashMap<Value, Held> = HashMap::new();
135            for inst in func.insts(block).collect::<Vec<Inst>>() {
136                match act(func, inst) {
137                    Act::Ignore => {}
138                    Act::Forget => known.clear(),
139                    Act::Wrote { address, value, ty } => {
140                        // Emptied first and recorded second, so that the store's own address
141                        // survives the clearing its own possible aliasing caused.
142                        known.clear();
143                        known.insert(address, Held { ty, value, stored: true });
144                    }
145                    Act::Read { address, result, ty } => {
146                        match known.get(&address).copied() {
147                            Some(held) if held.ty == ty => {
148                                if fuel.take() {
149                                    forward.insert(result, held.value);
150                                    gone.push(inst);
151                                    stats.optimized(if held.stored { FORWARDED } else { REUSED });
152                                    continue;
153                                }
154                                // Out of fuel, which is a request to stop transforming and not to
155                                // stop looking. The walk goes on so that the count of what could
156                                // have gone is the same at every fuel setting, which is what makes
157                                // a bisection over it monotonic.
158                                stats.missed(NO_FUEL);
159                            }
160                            Some(_) => stats.missed(WIDTH),
161                            None => {}
162                        }
163                        known.insert(address, Held { ty, value: result, stored: false });
164                    }
165                }
166            }
167        }
168
169        for inst in gone {
170            func.remove_inst(inst);
171        }
172        if !forward.is_empty() {
173            substitute(func, &forward);
174        }
175        stats
176    }
177}
178
179/// What the block knows about one address.
180///
181/// The value and the type are what the forwarding turns on. Whether it was stored or read is only
182/// for the counters, and they want it because the two numbers answer different questions. A
183/// forward from a store says the program wrote something and read it straight back, which is what
184/// an unrolled loop over an array looks like and what the constant folder can usually finish off.
185/// A reuse says the program read the same place twice, which is the `p->x` this pass is named for
186/// and which folds into nothing at all.
187#[derive(Clone, Copy)]
188struct Held {
189    /// The type the address is known at, which a load has to match exactly to be that value.
190    ty: Type,
191    /// What the address holds.
192    value: Value,
193    /// Whether a store put it there, rather than a load having read it.
194    stored: bool,
195}
196
197/// What one instruction does to the table.
198enum Act {
199    /// Nothing. It touches no memory.
200    Ignore,
201    /// It could write anywhere, so nothing the table says is known any more.
202    Forget,
203    /// It writes this value of this type at this address, and could have written anywhere else.
204    Wrote { address: Value, value: Value, ty: Type },
205    /// It reads a value of this type from this address into this result.
206    Read { address: Value, result: Value, ty: Type },
207}
208
209/// Which of the four an instruction is.
210///
211/// The two interesting cases are narrow on purpose. A plain non-volatile `Load` with one address
212/// and one result, and a plain non-volatile `Store` of one value to one address. `AtomicLoad` and
213/// `AtomicStore` are separate opcodes in this IR and are not these, so an ordering never reaches
214/// here as something to forward, and neither does a load carrying a memory token, which is what
215/// more than one result would mean.
216fn act(func: &Func, inst: Inst) -> Act {
217    let data = &func[inst];
218    if !data.opcode.touches_memory() {
219        return Act::Ignore;
220    }
221    if data.flags.contains(Flags::VOLATILE) {
222        return Act::Forget;
223    }
224    let args = &func[data.args];
225    match data.opcode {
226        Opcode::Load => {
227            let mut results = data.results();
228            let (Some(&address), Some(result), None) =
229                (args.first(), results.next(), results.next())
230            else {
231                return Act::Forget;
232            };
233            Act::Read { address, result, ty: func[result].ty }
234        }
235        Opcode::Store => {
236            let (Some(&value), Some(&address)) = (args.first(), args.get(1)) else {
237                return Act::Forget;
238            };
239            Act::Wrote { address, value, ty: func[value].ty }
240        }
241        _ => Act::Forget,
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use rucc_base::Interner;
248    use rucc_ir::{
249        Block, Builder, Extra, Flags, Func, InstData, MemInfo, MemOrder, Restrict, Signature, Type,
250        Value,
251    };
252
253    use super::*;
254    use crate::Fuel;
255
256    /// An empty function with one block, which is where every test below builds.
257    fn blank() -> (Interner, Func, Block) {
258        let mut names = Interner::new();
259        let name = names.intern("f");
260        let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(64)]));
261        let block = func.create_block();
262        (names, func, block)
263    }
264
265    /// An ordinary access of that alignment, with nothing said about its type.
266    fn plain(align: u32) -> MemInfo {
267        MemInfo {
268            size: 0,
269            align,
270            order: MemOrder::NotAtomic,
271            tbaa: None,
272            owns: 0,
273            restrict: Restrict::NONE,
274        }
275    }
276
277    /// An `alloca` of eight bytes, which is an address nothing outside the function knows.
278    fn local(build: &mut Builder<'_>) -> Value {
279        let mem = build.func().add_mem(MemInfo { size: 8, ..plain(8) });
280        build.value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR)
281    }
282
283    /// Runs the pass over the function with as much fuel as it wants.
284    fn run(func: &mut Func) -> Stats {
285        LoadForward.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
286    }
287
288    /// How many loads are left in the function.
289    fn loads(func: &Func) -> usize {
290        func.blocks()
291            .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
292            .filter(|&inst| func[inst].opcode == Opcode::Load)
293            .count()
294    }
295
296    /// What the return statement hands back, after the pass has pointed it somewhere.
297    fn returned(func: &Func) -> Vec<Value> {
298        let block = func.blocks().next().expect("the function has a block");
299        let inst = func.terminator(block).expect("the block has a terminator");
300        func[func[inst].args].to_vec()
301    }
302
303    #[test]
304    fn a_load_of_what_a_store_just_wrote_is_the_stored_value() {
305        let (_, mut func, block) = blank();
306        let mut build = Builder::new(&mut func, block);
307        let slot = local(&mut build);
308        let wrote = build.iconst(Type::int(64), 7);
309        build.store(wrote, slot, plain(8), Flags::NONE);
310        let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
311        build.ret(&[read]);
312
313        let stats = run(&mut func);
314        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
315        assert_eq!(loads(&func), 0, "the load itself has to go, nothing else would remove it");
316        assert_eq!(returned(&func), vec![wrote]);
317    }
318
319    #[test]
320    fn the_second_load_of_an_address_is_what_the_first_one_read() {
321        let (_, mut func, block) = blank();
322        let mut build = Builder::new(&mut func, block);
323        let slot = local(&mut build);
324        let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
325        let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
326        let sum = build.binary(Opcode::Add, first, second, Flags::NONE);
327        build.ret(&[sum]);
328
329        let stats = run(&mut func);
330        assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
331        assert_eq!(loads(&func), 1, "one read of that address has to happen and only one");
332        let sum = returned(&func)[0];
333        let rucc_ir::Def::Result { inst, .. } = func[sum].def else { panic!("the add is gone") };
334        assert_eq!(func[func[inst].args].to_vec(), vec![first, first]);
335    }
336
337    #[test]
338    fn a_store_of_a_different_width_is_not_forwarded_through() {
339        let (_, mut func, block) = blank();
340        let mut build = Builder::new(&mut func, block);
341        let slot = local(&mut build);
342        let wrote = build.iconst(Type::int(32), 7);
343        build.store(wrote, slot, plain(4), Flags::NONE);
344        let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
345        build.ret(&[read]);
346
347        let stats = run(&mut func);
348        assert_eq!(stats.count(crate::stats::Kind::Missed, WIDTH), 1);
349        assert_eq!(loads(&func), 1, "a four byte store does not say what eight bytes hold");
350        assert_eq!(returned(&func), vec![read]);
351    }
352
353    #[test]
354    fn a_call_between_the_two_accesses_is_a_write_to_everything() {
355        let (mut names, mut func, block) = blank();
356        let mut build = Builder::new(&mut func, block);
357        let slot = local(&mut build);
358        let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
359        let signature = build.func().add_signature(Signature::new());
360        build.call(names.intern("g"), signature, &[]);
361        let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
362        build.ret(&[first, second]);
363
364        let stats = run(&mut func);
365        assert!(!stats.changed(), "nothing here says what the call did to that address");
366        assert_eq!(loads(&func), 2);
367    }
368
369    #[test]
370    fn a_store_to_another_address_is_a_write_to_everything_too() {
371        let (_, mut func, block) = blank();
372        let mut build = Builder::new(&mut func, block);
373        let slot = local(&mut build);
374        let other = local(&mut build);
375        let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
376        build.store(first, other, plain(8), Flags::NONE);
377        let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
378        build.ret(&[first, second]);
379
380        // Two allocas cannot be the same object, so this is an opportunity and not a hazard. It is
381        // left on the table on purpose: telling the two apart is the alias oracle's answer and
382        // this pass is the version that does not have one.
383        let stats = run(&mut func);
384        assert!(!stats.changed());
385        assert_eq!(loads(&func), 2);
386    }
387
388    #[test]
389    fn a_volatile_load_is_not_reused_and_nothing_before_it_survives_it() {
390        let (_, mut func, block) = blank();
391        let mut build = Builder::new(&mut func, block);
392        let slot = local(&mut build);
393        let first = build.load(Type::int(64), slot, plain(8), Flags::VOLATILE);
394        let second = build.load(Type::int(64), slot, plain(8), Flags::VOLATILE);
395        let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
396        build.ret(&[first, second, third]);
397
398        let stats = run(&mut func);
399        assert!(!stats.changed(), "every volatile read has to happen");
400        assert_eq!(loads(&func), 3);
401    }
402
403    #[test]
404    fn what_one_block_knows_does_not_reach_the_next_one() {
405        let (_, mut func, entry) = blank();
406        let next = func.create_block();
407        let mut build = Builder::new(&mut func, entry);
408        let slot = local(&mut build);
409        let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
410        build.jump(next, &[]);
411        let mut build = Builder::new(&mut func, next);
412        let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
413        build.ret(&[first, second]);
414
415        // The value is available and the `-O2` version of this pass finds it. Section 16.2 is
416        // explicit that this one does not, because reaching it means memory SSA.
417        let stats = run(&mut func);
418        assert!(!stats.changed());
419        assert_eq!(loads(&func), 2);
420    }
421
422    #[test]
423    fn a_chain_of_reads_all_come_from_the_first_one() {
424        let (_, mut func, block) = blank();
425        let mut build = Builder::new(&mut func, block);
426        let slot = local(&mut build);
427        let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
428        build.load(Type::int(64), slot, plain(8), Flags::NONE);
429        let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
430        build.ret(&[third]);
431
432        let stats = run(&mut func);
433        assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 2);
434        assert_eq!(loads(&func), 1);
435        assert_eq!(returned(&func), vec![first]);
436    }
437
438    #[test]
439    fn a_store_of_a_value_the_pass_is_removing_forwards_to_where_that_value_went() {
440        let (_, mut func, block) = blank();
441        let mut build = Builder::new(&mut func, block);
442        let slot = local(&mut build);
443        let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
444        let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
445        build.store(second, slot, plain(8), Flags::NONE);
446        let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
447        build.ret(&[third]);
448
449        // The store writes a value this pass is in the middle of taking away, so the third load
450        // has to land one step further back than what the table says. Landing on the second load's
451        // result would leave the function reading an instruction that is no longer in it.
452        let stats = run(&mut func);
453        assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
454        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
455        assert_eq!(loads(&func), 1);
456        assert_eq!(returned(&func), vec![first]);
457    }
458
459    #[test]
460    fn without_fuel_the_load_stays_and_the_chance_is_still_counted() {
461        let (_, mut func, block) = blank();
462        let mut build = Builder::new(&mut func, block);
463        let slot = local(&mut build);
464        let wrote = build.iconst(Type::int(64), 7);
465        build.store(wrote, slot, plain(8), Flags::NONE);
466        let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
467        build.ret(&[read]);
468
469        let stats =
470            LoadForward.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::of(0));
471        assert!(!stats.changed());
472        assert_eq!(stats.count(crate::stats::Kind::Missed, NO_FUEL), 1);
473        assert_eq!(loads(&func), 1);
474    }
475}