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//! # The same address read twice
51//!
52//! A load is not a def of memory, so the walk goes straight past an earlier load of the same
53//! address and arrives at whatever wrote it last, which is often nothing it can name. That leaves
54//! the easiest redundant load of all in place: two loads of the same address at the same version of
55//! memory read the same bytes, and the version of memory is exactly the statement that nothing
56//! wrote them in between.
57//!
58//! So there is a table alongside the walk, keyed by the version of memory, the address and the
59//! type, holding what the first load of that key is known to be equal to, and a later load whose
60//! key is in it takes that value. It is value numbering over memory rather than a walk, which is
61//! why it is a table here and not another [`Clobber`] variant in [`crate::memssa`].
62//!
63//! Two things make it right. Memory is threaded through every instruction that touches it, so two
64//! loads carrying the same version have no write between them on any path from the one to the
65//! other. That is a statement about paths through the earlier load, so the earlier load has to be
66//! on every path to the later one, and that is dominance and is the one thing this pass computes
67//! that the walk did not need. On a safety build the two are usually separated by a check, and a
68//! check reads the planes and writes nothing, so the version survives it.
69//!
70//! What goes in the table is the value the load is equal to rather than the load's own result. A
71//! load that was itself forwarded is about to be removed, [`substitute`] does not chase a rewrite
72//! through another rewrite, and a later load of the same key can still reach the table when its own
73//! walk was the one that ran out of budget.
74//!
75//! # A load followed through a copy
76//!
77//! A struct assignment lowers to a copy, so a field read after one is a load whose walk stops at a
78//! `memcpy` with the value it wants sitting in the copy's source. Section 9.2's answer to that is
79//! `translate`: the walk hands the reference and the def in the way to the caller, and a caller
80//! that can see past the def rewrites the reference and the walk carries on with the new one.
81//! [`Walk::clobber_with`] has taken the callback since it was written and this is what fills it in,
82//! by asking about the same distance into the copy's source instead. `through` says what has to be
83//! known for that to be the same bytes rather than a guess.
84//!
85//! # What is not here
86//!
87//! Phi translation, which is asking about a load whose address is a block parameter in the terms of
88//! the predecessor the walk is going into. It was built and measured, and over four libraries at
89//! `-O2` with and without `-fsafety=detect` it forwarded not one load that was not already being
90//! forwarded and emitted the same bytes. Section 9.2 of `spec/optimizer/09-memory-ssa.md` has the
91//! numbers and the reason, so the numbers are what this repository keeps rather than the code.
92//!
93//! # The chain goes on and comes off again
94//!
95//! [`memssa::build`] before and [`memssa::strip`] after, per function. The back end has never seen
96//! memory SSA and is not going to, and nothing in the pipeline keeps the chain across passes,
97//! because that would mean every edit to the control flow graph anywhere in the optimizer had to
98//! keep the memory parameters in step with the blocks. Two linear walks per function is the price
99//! of not making that claim.
100//!
101//! It has one consequence worth naming. An instruction cannot grow or lose a result, so putting the
102//! chain on and taking it off again replaces every instruction that touches memory with an
103//! equivalent one, even in a function where not a single load was forwarded. The shape of the
104//! function is untouched, so every control flow answer still stands, and the liveness is about
105//! values and does not, which is why this pass drops it whether or not it changed anything.
106
107use std::collections::HashMap;
108
109use rucc_ir::{Block, Extra, Flags, Func, Inst, Opcode, Restrict, Type, Value};
110
111use crate::alias::{Access, origin};
112use crate::memssa::{Clobber, Step, Walk};
113use crate::uses::substitute;
114use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats, memssa};
115
116/// What this pass is called, which the pipeline matches on to decide whether to build the module
117/// facts the oracle asks for.
118pub const NAME: &str = "redundant-load";
119
120/// Recorded for a load that took the value of the store the walk said it sees.
121const FORWARDED: &str = "load replaced by the value of the store the walk found";
122
123/// Recorded for a load that took the value an earlier load of the same address already had.
124const REUSED: &str = "load replaced by the value an earlier load of the same address already had";
125
126/// Recorded for a load the walk placed on a write that covered only part of it.
127const PARTIAL: &str = "load kept, what wrote it covers only part of what it reads";
128
129/// Recorded for a load the walk placed on a write it could not pin down.
130const MAYBE: &str = "load kept, something that may have written it could not be pinned down";
131
132/// Recorded for a load whose walk ran out of budget or reached a join whose paths disagreed.
133const UNKNOWN: &str = "load kept, the walk back over memory established nothing";
134
135/// Recorded for a load whose store covered the same bytes at a different type.
136const WIDTH: &str = "load kept, the store that covers it wrote a different type";
137
138/// Recorded for a load the walk placed on a write that is not a store of one value.
139const NOT_A_STORE: &str = "load kept, what covers it writes memory without storing one value";
140
141/// Recorded for a load that would have gone if there had been fuel for it.
142const NO_FUEL: &str = "redundant load kept, the pass ran out of fuel";
143
144/// The pass.
145#[derive(Debug)]
146pub struct RedundantLoad;
147
148impl Pass for RedundantLoad {
149    fn name(&self) -> &'static str {
150        NAME
151    }
152
153    fn describe(&self) -> &'static str {
154        "a load takes the value of the store it sees, wherever in the function that store is"
155    }
156
157    fn preserves(&self) -> Preserved {
158        // The shape of the function, for the reason `crate::load` gives: no block is added, none
159        // is removed, no edge moves, and the instructions that go are loads, which are never
160        // terminators. The memory parameters this puts on the joins come back off before the pass
161        // returns, so no block ends with a parameter it did not start with.
162        Preserved::ALL.without(Analysis::Liveness)
163    }
164
165    fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
166        let mut stats = Stats::new();
167        if !memssa::build(func) {
168            return stats;
169        }
170        // What each removed load's result is read as, applied to the whole function once at the
171        // end, for the reason `crate::load` gives: rewriting each one where it is found would be a
172        // walk over the function per load and there is nothing to gain by it.
173        let mut forward: HashMap<Value, Value> = HashMap::new();
174        let mut gone: Vec<Inst> = Vec::new();
175
176        // The walk borrows the function, so it is a scope of its own and every edit happens after
177        // it. Built once, because the alias oracle inside it holds an escape analysis that is one
178        // walk over the function and every step of every walk may ask it.
179        {
180            // The walk holds the function and so does the callback it is handed, so there is a name
181            // for the shared borrow of it rather than two reborrows of the same `&mut` written
182            // where a reader has to work out that they are both reads.
183            let body: &Func = func;
184            let dom = an.dominators(body);
185            let mut walk = Walk::new(body, an.outside()).knowing(an.modref());
186            // One entry per address read at a version of memory, holding the block the first load
187            // of it was in and the value that load is known to be equal to.
188            let mut seen: HashMap<(Value, Value, Type), (Block, Value)> = HashMap::new();
189            for block in func.blocks().collect::<Vec<Block>>() {
190                for inst in func.insts(block).collect::<Vec<Inst>>() {
191                    let Some((result, ty)) = reads(func, inst) else {
192                        continue;
193                    };
194                    let key = func.mem_in(inst).map(|mem| (mem, func[func[inst].args][0], ty));
195                    let found = match walk.clobber_with(inst, &mut |reference, def| {
196                        through(body, reference, def).map_or(Step::Stop, Step::Retry)
197                    }) {
198                        Clobber::Exact(wrote) => match stored(func, wrote) {
199                            None => Found::Kept(Some(NOT_A_STORE)),
200                            Some(value) if func[value].ty != ty => Found::Kept(Some(WIDTH)),
201                            Some(value) => Found::Store(value),
202                        },
203                        Clobber::Partial(_) => Found::Kept(Some(PARTIAL)),
204                        Clobber::Maybe(_) => Found::Kept(Some(MAYBE)),
205                        Clobber::Unknown => Found::Kept(Some(UNKNOWN)),
206                        // Nothing in the function wrote it, so the load reads whatever was there
207                        // when the function started. There is no value here to take and nothing
208                        // was missed either, so it is not counted as one.
209                        Clobber::NoClobber => Found::Kept(None),
210                    };
211                    // The walk had nothing, so ask whether an earlier load of the same address at
212                    // this version of memory had something. The dominance is what the walk did not
213                    // have to compute and this does: two arms of the same branch share a version of
214                    // memory and neither of them runs before the other.
215                    let found = match found {
216                        Found::Kept(reason) => match key.and_then(|key| seen.get(&key)) {
217                            Some(&(at, value)) if dom.dominates(at, block) => Found::Earlier(value),
218                            _ => Found::Kept(reason),
219                        },
220                        taken => taken,
221                    };
222                    let (value, why) = match found {
223                        Found::Store(value) => (value, FORWARDED),
224                        Found::Earlier(value) => (value, REUSED),
225                        Found::Kept(reason) => {
226                            if let Some(reason) = reason {
227                                stats.missed(reason);
228                            }
229                            remember(&mut seen, key, block, result);
230                            continue;
231                        }
232                    };
233                    if !fuel.take() {
234                        // Out of fuel is a request to stop transforming and not to stop looking, so
235                        // the walk goes on and the count of what could have gone is the same at
236                        // every setting, which is what makes a bisection over it monotonic.
237                        stats.missed(NO_FUEL);
238                        remember(&mut seen, key, block, result);
239                        continue;
240                    }
241                    forward.insert(result, value);
242                    gone.push(inst);
243                    stats.optimized(why);
244                    remember(&mut seen, key, block, value);
245                }
246            }
247            let counts = walk.counts();
248            if counts.walks() > 0 {
249                stats.record(crate::stats::Kind::Note, WALKS, count(counts.walks()));
250                stats.record(crate::stats::Kind::Note, STEPS, count(counts.steps()));
251                if counts.exhausted() > 0 {
252                    stats.record(crate::stats::Kind::Note, EXHAUSTED, count(counts.exhausted()));
253                }
254                if counts.rewritten() > 0 {
255                    stats.record(crate::stats::Kind::Note, REWRITTEN, count(counts.rewritten()));
256                }
257            }
258        }
259
260        for inst in gone {
261            func.remove_inst(inst);
262        }
263        if !forward.is_empty() {
264            substitute(func, &forward);
265        }
266        memssa::strip(func);
267        // Said here rather than left to `preserves`, because the manager takes a pass that changed
268        // nothing to have preserved everything and this one has not: the chain going on and coming
269        // off gives every instruction that touches memory a new name whatever the pass did with
270        // them.
271        an.settle(func, self.preserves(), false);
272        stats
273    }
274}
275
276/// The bytes a copy took its answer from, for a load the copy covered all of.
277///
278/// Section 9.2's `translate`, and the one case that section says is worth having it for. A copy
279/// moves a run of bytes, so the value at a place inside the destination is the value that was at
280/// the same distance into the source when the copy ran, and asking the walk to carry on from the
281/// copy about that place is asking what the source held then.
282///
283/// Everything has to be known or the answer is nothing, which is what keeps this from being a
284/// guess. Both ends have to be an object the walk could name at a fixed distance into it. The load
285/// has to be inside what the copy wrote rather than across its edge, since a load half covered by a
286/// copy is half the source and half whatever the destination held before. And the reference has to
287/// be about the object the copy wrote rather than one the oracle merely could not tell apart from
288/// it, because a `may` is the reason to stop rather than a reason to follow.
289///
290/// `memmove` is here alongside `memcpy`. An overlapping move puts the bytes that were in the source
291/// into the destination the same way a copy does, and what this asks for is what the source held
292/// before the move, which is the version of memory the walk carries on from.
293///
294/// The new reference carries neither the type node nor the `restrict` scope. A copy moves bytes and
295/// says nothing about what they are, so the load's type node is about the destination, and letting
296/// it rule out a write to the source is exactly the rewrite section 9.6 calls the subtlest bug in
297/// that document.
298fn through(func: &Func, reference: &Access, inst: Inst) -> Option<Access> {
299    let data = func[inst];
300    if !matches!(data.opcode, Opcode::Memcpy | Opcode::Memmove)
301        || data.flags.contains(Flags::VOLATILE)
302    {
303        return None;
304    }
305    let Extra::Mem(info) = data.extra else {
306        return None;
307    };
308    let args = &func[data.args];
309    let (&to, &from) = (args.first()?, args.get(1)?);
310    let (to_origin, Some(to_offset)) = origin(func, to) else {
311        return None;
312    };
313    let (from_origin, Some(from_offset)) = origin(func, from) else {
314        return None;
315    };
316    if reference.origin != to_origin {
317        return None;
318    }
319    let (start, end) = reference.range()?;
320    let (wrote, length) = (i128::from(to_offset), i128::from(func[info].size));
321    if start < wrote || end > wrote + length {
322        return None;
323    }
324    Some(Access {
325        origin: from_origin,
326        offset: Some(i64::try_from(i128::from(from_offset) + (start - wrote)).ok()?),
327        size: reference.size,
328        tbaa: None,
329        restrict: Restrict::NONE,
330        volatile: reference.volatile,
331    })
332}
333
334/// What there is to put in place of a load, and where it came from.
335enum Found {
336    /// The store the walk arrived at wrote this.
337    Store(Value),
338    /// An earlier load of the same address at the same version of memory already had this.
339    Earlier(Value),
340    /// The load stays, with the reason when there is one worth recording.
341    Kept(Option<&'static str>),
342}
343
344/// Records what a load of this address at this version of memory is equal to.
345///
346/// The first one of a key wins. A second one is either dominated by the first, in which case it was
347/// forwarded and there is nothing left to record, or it is not, and then neither block dominates
348/// the other and keeping the one already there is as good as swapping it.
349fn remember(
350    seen: &mut HashMap<(Value, Value, Type), (Block, Value)>,
351    key: Option<(Value, Value, Type)>,
352    block: Block,
353    value: Value,
354) {
355    if let Some(key) = key {
356        seen.entry(key).or_insert((block, value));
357    }
358}
359
360/// Recorded as a note: how many walks were made.
361const WALKS: &str = "walks back over memory";
362
363/// Recorded as a note: how many defs those walks looked at, which is one alias query each.
364const STEPS: &str = "memory defs the walks looked at";
365
366/// Recorded as a note: how many walks gave up rather than answering.
367///
368/// Section 9.3 of `spec/optimizer/09-memory-ssa.md` says this number decides whether the walk gets
369/// a cache. Above one percent of walks and the budget is too small or the alias analysis is too
370/// weak, and both of those are better fixed than cached around.
371const EXHAUSTED: &str = "walks that ran out of budget";
372
373/// Recorded as a note: how many times a walk carried on with the reference `through` rewrote.
374///
375/// Next to the step count because a rewrite starts a walk again and the steps are where that
376/// shows, and on its own because it is the only thing that says whether following a load through
377/// a copy is reaching anything on this build at all.
378const REWRITTEN: &str = "references rewritten to what a copy took them from";
379
380/// A count as the record holds them, which is narrower than the counters are.
381fn count(of: u64) -> u32 {
382    u32::try_from(of).unwrap_or(u32::MAX)
383}
384
385/// The result and the type of a load worth asking about, and nothing for anything else.
386///
387/// Narrow on purpose, and the same shape [`crate::load`] uses. A plain non-volatile `Load` with one
388/// address and one result. `AtomicLoad` is a separate opcode in this IR and is not this one, so an
389/// ordering never reaches here as something to forward.
390fn reads(func: &Func, inst: Inst) -> Option<(Value, Type)> {
391    let data = &func[inst];
392    if data.opcode != Opcode::Load || data.flags.contains(Flags::VOLATILE) {
393        return None;
394    }
395    let mut results = data.results();
396    let (Some(result), None) = (results.next(), results.next()) else {
397        return None;
398    };
399    Some((result, func[result].ty))
400}
401
402/// What a store wrote, and nothing for anything else that writes memory.
403///
404/// The walk answers `Exact` for any write that covered exactly the bytes the load reads, and a
405/// `memcpy` or a `memset` can do that without there being one value anywhere to take.
406fn stored(func: &Func, inst: Inst) -> Option<Value> {
407    let data = &func[inst];
408    if data.opcode != Opcode::Store || data.flags.contains(Flags::VOLATILE) {
409        return None;
410    }
411    func[data.args].first().copied()
412}
413
414#[cfg(test)]
415mod tests {
416    use std::sync::Arc;
417
418    use rucc_base::Interner;
419    use rucc_ir::{Module, parse, verify_func};
420
421    use super::*;
422    use crate::outside::Outside;
423
424    const HEADER: &str = "\
425; ModuleID = 'mem.c'
426; format 0
427target triple = \"x86_64-unknown-linux-gnu\"
428target datalayout = \"e-p:64:64-i64:64-f80:128-S128\"
429";
430
431    fn wrap(signature: &str, body: &str) -> String {
432        format!("{HEADER}\nfunc @f{signature}, linkage(external) {{\n{body}}}\n")
433    }
434
435    /// Runs the pass over the one function in the text and insists the result verifies, which is
436    /// where most of the strength of these tests is: the chain goes on and comes off again, and a
437    /// half removed chain is exactly the kind of thing a shape assertion would let through.
438    fn run(text: &str) -> (Module, Stats) {
439        let mut names = Interner::new();
440        let mut module = parse(text, &mut names).expect("the text parses");
441        let id = module.funcs().next().expect("one function");
442        let outside = Arc::new(Outside::of(&module));
443        let mut an = crate::machine::fixtures::analyses().about(outside);
444        let stats = RedundantLoad.run(&mut module[id], &mut an, &mut Fuel::unlimited());
445        if let Err(errors) = verify_func(&module, &module[id], &names) {
446            panic!("{errors:#?}");
447        }
448        (module, stats)
449    }
450
451    fn one(module: &Module) -> &Func {
452        &module[module.funcs().next().expect("one function")]
453    }
454
455    /// How many instructions with that opcode the function has left.
456    fn count_of(func: &Func, opcode: Opcode) -> usize {
457        func.blocks()
458            .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
459            .filter(|&inst| func[inst].opcode == opcode)
460            .count()
461    }
462
463    /// Nothing anywhere in the function is on the chain.
464    fn off(func: &Func) {
465        for block in func.blocks() {
466            assert!(func[block].params.iter().all(|&param| !func[param].ty.is_mem()));
467            for inst in func.insts(block) {
468                assert_ne!(func[inst].opcode, Opcode::MemEntry);
469                assert!(!func.carries_mem(inst));
470            }
471        }
472    }
473
474    #[test]
475    fn a_store_in_a_block_above_the_load_reaches_it() {
476        // The case the one block version was built not to handle, and the reason this pass is
477        // worth its two walks.
478        let text = wrap(
479            "(ptr, i1) -> i32",
480            "block0(%0: ptr, %1: i1):
481    %2 = iconst.i32 7
482    store %2 -> %0, align 4
483    br_if %1, block1, block2
484
485block1:
486    jump block2
487
488block2:
489    %3 = load.i32 %0, align 4
490    return %3
491",
492        );
493        let (module, stats) = run(&text);
494        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
495        let func = one(&module);
496        off(func);
497        assert_eq!(count_of(func, Opcode::Load), 0, "the load is still there");
498        // What the function returns is now the constant the store wrote.
499        let ret = func
500            .blocks()
501            .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
502            .find(|&inst| func[inst].opcode == Opcode::Return)
503            .expect("a return");
504        let returned = func[func[ret].args][0];
505        let seven = func
506            .blocks()
507            .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
508            .find(|&inst| func[inst].opcode == Opcode::IConst)
509            .expect("the constant");
510        assert_eq!(returned, func[seven].results().next().expect("a result"));
511    }
512
513    #[test]
514    fn a_swap_down_one_arm_is_not_an_answer_for_the_join_below_it() {
515        // Issue 1568, and the shape is the swap in `runtime/builtins/quad.c`: three copies down one
516        // arm that exchange two objects, and a read of one of them below the join. Down the arm
517        // that swapped, the first object holds what the second one held, and down the other arm it
518        // holds what it was given, so the two paths disagree and the load has to stay.
519        //
520        // What made it an answer was the visited set. The walk over the arm that swapped rewrites
521        // the question at each copy, and it used to carry the set across the rewrite, so it marked
522        // versions in the block above the branch as seen while asking about somewhere else. The
523        // walk over the other arm then reached those same versions, was told it had been there,
524        // and gave nothing back, and nothing on one path into a join is whatever the other path
525        // said. The join came back holding the swapped arm's answer alone.
526        let text = wrap(
527            "(i1) -> i64",
528            "block0(%0: i1):
529    %1 = alloca, size 16, align 8
530    %2 = alloca, size 16, align 8
531    %3 = alloca, size 16, align 8
532    %4 = iconst.i64 11
533    store %4 -> %1, align 8
534    %5 = iconst.i64 22
535    store %5 -> %2, align 8
536    br_if %0, block1, block2
537
538block1:
539    memcpy %3, %1, size 16, align 8
540    memcpy %1, %2, size 16, align 8
541    memcpy %2, %3, size 16, align 8
542    jump block3
543
544block2:
545    jump block3
546
547block3:
548    %6 = load.i64 %1, align 8
549    return %6
550",
551        );
552        let (module, stats) = run(&text);
553        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 0);
554        assert_eq!(count_of(one(&module), Opcode::Load), 1);
555    }
556
557    #[test]
558    fn a_store_down_only_one_arm_is_not_an_answer() {
559        // One path into the join wrote it and the other did not, and a disagreement is `Unknown`
560        // rather than the weaker of the two, because there is no order on these to act on.
561        let text = wrap(
562            "(ptr, i1) -> i32",
563            "block0(%0: ptr, %1: i1):
564    br_if %1, block1, block2
565
566block1:
567    %2 = iconst.i32 7
568    store %2 -> %0, align 4
569    jump block3
570
571block2:
572    jump block3
573
574block3:
575    %3 = load.i32 %0, align 4
576    return %3
577",
578        );
579        let (module, stats) = run(&text);
580        assert!(!stats.changed(), "a store on one path is not the value on both");
581        assert_eq!(stats.count(crate::stats::Kind::Missed, UNKNOWN), 1);
582        off(one(&module));
583    }
584
585    #[test]
586    fn both_arms_storing_the_same_way_is_still_two_stores() {
587        // Two stores of the same value are two instructions, so the paths name two different
588        // clobbers and disagree. Taking this one wants value numbering over the stores rather
589        // than a better walk, and it is worth a test saying which of the two it needs.
590        let text = wrap(
591            "(ptr, i1) -> i32",
592            "block0(%0: ptr, %1: i1):
593    %2 = iconst.i32 7
594    br_if %1, block1, block2
595
596block1:
597    store %2 -> %0, align 4
598    jump block3
599
600block2:
601    store %2 -> %0, align 4
602    jump block3
603
604block3:
605    %3 = load.i32 %0, align 4
606    return %3
607",
608        );
609        let (_, stats) = run(&text);
610        assert!(!stats.changed());
611    }
612
613    #[test]
614    fn a_loop_that_writes_nothing_keeps_the_store_above_it() {
615        // The back edge leads to the parameter the walk started from, which is how a cycle is cut
616        // and contributes nothing, so what is left is the one path that wrote it.
617        let text = wrap(
618            "(ptr, i1) -> i32",
619            "block0(%0: ptr, %1: i1):
620    %2 = iconst.i32 7
621    store %2 -> %0, align 4
622    jump block1
623
624block1:
625    %3 = load.i32 %0, align 4
626    br_if %1, block1, block2
627
628block2:
629    return %3
630",
631        );
632        let (module, stats) = run(&text);
633        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
634        assert_eq!(count_of(one(&module), Opcode::Load), 0);
635    }
636
637    #[test]
638    fn a_store_inside_the_loop_stops_it() {
639        let text = wrap(
640            "(ptr, i1) -> i32",
641            "block0(%0: ptr, %1: i1):
642    %2 = iconst.i32 7
643    store %2 -> %0, align 4
644    jump block1
645
646block1:
647    %3 = load.i32 %0, align 4
648    %4 = add %3, %3
649    store %4 -> %0, align 4
650    br_if %1, block1, block2
651
652block2:
653    return %3
654",
655        );
656        let (_, stats) = run(&text);
657        assert!(!stats.changed(), "the body writes what the load reads");
658    }
659
660    #[test]
661    fn the_bytes_being_the_same_is_not_the_type_being_the_same() {
662        // Section 16.6's most likely wrong answer. Four bytes stored and four bytes read is the
663        // same run of memory and two different readings of it, and this pass takes neither.
664        let text = wrap(
665            "(ptr) -> f32",
666            "block0(%0: ptr):
667    %1 = iconst.i32 7
668    store %1 -> %0, align 4
669    %2 = load.f32 %0, align 4
670    return %2
671",
672        );
673        let (_, stats) = run(&text);
674        assert!(!stats.changed());
675        assert_eq!(stats.count(crate::stats::Kind::Missed, WIDTH), 1);
676    }
677
678    #[test]
679    fn the_same_address_read_twice_over_is_read_once() {
680        // Nothing in the function writes memory at all, so the walk says nobody wrote it for both
681        // of these and has no value for either. The two of them are still the same value.
682        let text = wrap(
683            "(ptr) -> i32",
684            "block0(%0: ptr):
685    %1 = load.i32 %0, align 4
686    %2 = load.i32 %0, align 4
687    %3 = add %1, %2
688    return %3
689",
690        );
691        let (module, stats) = run(&text);
692        assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
693        let func = one(&module);
694        off(func);
695        assert_eq!(count_of(func, Opcode::Load), 1);
696    }
697
698    #[test]
699    fn a_check_between_them_is_not_a_write() {
700        // What this is worth on a safety build, which is the shape above with the check the
701        // instrumentation puts in front of the second access. A check reads the planes and writes
702        // nothing, so the version of memory the second load carries is the first one's.
703        let text = wrap(
704            "(ptr) -> i32",
705            "block0(%0: ptr):
706    %1 = load.i32 %0, align 4
707    %2 = cap_of %0
708    check_bounds %2, %0, size 4, align 4
709    %3 = load.i32 %0, align 4
710    %4 = add %1, %3
711    return %4
712",
713        );
714        let (module, stats) = run(&text);
715        assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
716        let func = one(&module);
717        assert_eq!(count_of(func, Opcode::Load), 1);
718        assert_eq!(count_of(func, Opcode::CheckBounds), 1);
719    }
720
721    #[test]
722    fn a_write_that_may_be_the_same_address_ends_it() {
723        // The two pointers are two parameters and neither of them is restrict, so the store may be
724        // to the same place. The store is a def of memory, so the second load carries a version the
725        // first one never had and the table cannot match it, which is the check being the version
726        // rather than a list of what the pass thinks is in the way.
727        let text = wrap(
728            "(ptr, ptr) -> i32",
729            "block0(%0: ptr, %1: ptr):
730    %2 = load.i32 %0, align 4
731    %3 = iconst.i32 7
732    store %3 -> %1, align 4
733    %4 = load.i32 %0, align 4
734    %5 = add %2, %4
735    return %5
736",
737        );
738        let (module, stats) = run(&text);
739        assert!(!stats.changed());
740        assert_eq!(count_of(one(&module), Opcode::Load), 2);
741    }
742
743    #[test]
744    fn a_setjmp_between_them_ends_it_even_for_a_local_nobody_else_can_see() {
745        // Issue 1503. The alloca is private, so every argument about what a call can reach says
746        // this store reaches this load, and for a call every one of them would be right. The
747        // marker is not a call. Control reaches the load from wherever the matching `longjmp` was
748        // as well as from the store above it, and what the jump left in the slot is the whole
749        // point of the program, so forwarding the store here is the miscompilation where a
750        // function returns the value it started with instead of the one it was jumped back with.
751        let text = wrap(
752            "(ptr) -> i32",
753            "block0(%0: ptr):
754    %1 = alloca, size 4, align 4
755    %2 = iconst.i32 0
756    store %2 -> %1, align 4
757    %3 = setjmp_marker.i32 %0
758    %4 = load.i32 %1, align 4
759    return %4
760",
761        );
762        let (module, stats) = run(&text);
763        assert!(!stats.changed());
764        assert_eq!(count_of(one(&module), Opcode::Load), 1);
765    }
766
767    #[test]
768    fn one_arm_reading_it_is_not_the_other_arm_having_read_it() {
769        // Nothing here writes memory, so both loads carry the version the function started with and
770        // the table matches. Neither block runs before the other, which is what the dominance is
771        // there to say, and without it this is a use of a value that is not in scope.
772        let text = wrap(
773            "(ptr, i1) -> i32",
774            "block0(%0: ptr, %1: i1):
775    br_if %1, block1, block2
776
777block1:
778    %2 = load.i32 %0, align 4
779    jump block3(%2)
780
781block2:
782    %3 = load.i32 %0, align 4
783    jump block3(%3)
784
785block3(%4: i32):
786    return %4
787",
788        );
789        let (module, stats) = run(&text);
790        assert!(!stats.changed());
791        assert_eq!(count_of(one(&module), Opcode::Load), 2);
792    }
793
794    #[test]
795    fn a_load_above_the_branch_reaches_both_arms() {
796        // The same shape the other way up, where the first load is on every path to the other two.
797        let text = wrap(
798            "(ptr, i1) -> i32",
799            "block0(%0: ptr, %1: i1):
800    %2 = load.i32 %0, align 4
801    br_if %1, block1, block2
802
803block1:
804    %3 = load.i32 %0, align 4
805    jump block3(%3)
806
807block2:
808    %4 = load.i32 %0, align 4
809    jump block3(%4)
810
811block3(%5: i32):
812    %6 = add %2, %5
813    return %6
814",
815        );
816        let (module, stats) = run(&text);
817        assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 2);
818        let func = one(&module);
819        off(func);
820        assert_eq!(count_of(func, Opcode::Load), 1);
821    }
822
823    #[test]
824    fn a_field_read_after_a_struct_assignment_comes_out_of_the_source() {
825        // What `translate` is for. The copy is from part way into one object to the start of
826        // another and the load is part way into that, so the place asked about on the other side
827        // of the copy is neither operand's own offset and the arithmetic has to be right.
828        let text = wrap(
829            "() -> i32",
830            "block0:
831    %0 = alloca, size 32, align 8
832    %1 = alloca, size 16, align 8
833    %2 = iconst.i64 12
834    %3 = ptr_add %0, %2
835    %4 = iconst.i32 7
836    store %4 -> %3, align 4
837    %5 = iconst.i64 8
838    %6 = ptr_add %0, %5
839    memcpy %1, %6, size 8, align 8
840    %7 = iconst.i64 4
841    %8 = ptr_add %1, %7
842    %9 = load.i32 %8, align 4
843    return %9
844",
845        );
846        let (module, stats) = run(&text);
847        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
848        let func = one(&module);
849        off(func);
850        assert_eq!(count_of(func, Opcode::Load), 0);
851        assert_eq!(
852            count_of(func, Opcode::Memcpy),
853            1,
854            "the copy itself is not this pass's to remove"
855        );
856    }
857
858    #[test]
859    fn the_plane_writes_a_copy_leaves_behind_do_not_stop_the_walk() {
860        // The same shape as the test above with what `-fsafety=detect` puts after a copy in it,
861        // which is what the compiler actually optimizes, since the safety opcodes are still
862        // opcodes when this pass runs and are lowered to calls afterwards.
863        let text = wrap(
864            "() -> i32",
865            "block0:
866    %0 = alloca, size 32, align 8
867    %1 = alloca, size 16, align 8
868    %2 = iconst.i64 12
869    %3 = ptr_add %0, %2
870    %4 = iconst.i32 7
871    store %4 -> %3, align 4
872    %5 = iconst.i64 8
873    %6 = ptr_add %0, %5
874    memcpy %1, %6, size 8, align 8
875    %7 = iconst.i64 8
876    meta_type_copy %1, %6, %7
877    meta_init_copy %1, %6, %7
878    cap_copy %1, %6, %7
879    %8 = iconst.i64 4
880    %9 = ptr_add %1, %8
881    %10 = load.i32 %9, align 4
882    return %10
883",
884        );
885        let (module, stats) = run(&text);
886        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
887        let func = one(&module);
888        off(func);
889        assert_eq!(count_of(func, Opcode::Load), 0);
890    }
891
892    #[test]
893    fn what_the_copy_moved_is_what_the_source_held_when_it_ran() {
894        // The store after the copy is not the answer, and it is the one that would come back if
895        // the walk carried on from the load rather than from the copy.
896        let text = wrap(
897            "() -> i32",
898            "block0:
899    %0 = alloca, size 4, align 4
900    %1 = alloca, size 4, align 4
901    %2 = iconst.i32 7
902    store %2 -> %0, align 4
903    memcpy %1, %0, size 4, align 4
904    %3 = iconst.i32 9
905    store %3 -> %0, align 4
906    %4 = load.i32 %1, align 4
907    return %4
908",
909        );
910        let (module, stats) = run(&text);
911        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
912        let func = one(&module);
913        assert_eq!(count_of(func, Opcode::IConst), 2);
914        let ret = func
915            .blocks()
916            .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
917            .find(|&inst| func[inst].opcode == Opcode::Return)
918            .expect("a return");
919        let seven = func
920            .blocks()
921            .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
922            .find(|&inst| func[inst].opcode == Opcode::IConst)
923            .expect("the first constant, which is the one stored before the copy");
924        assert_eq!(func[func[ret].args][0], func[seven].results().next().expect("a result"));
925    }
926
927    #[test]
928    fn a_load_across_the_edge_of_a_copy_stays() {
929        // Half of what this reads came through the copy and half of it was already there, and a
930        // reference rewritten to the source would be asking about the wrong two bytes as well as
931        // about the right two.
932        let text = wrap(
933            "() -> i32",
934            "block0:
935    %0 = alloca, size 8, align 4
936    %1 = alloca, size 8, align 4
937    %2 = iconst.i32 7
938    store %2 -> %0, align 4
939    memcpy %1, %0, size 4, align 4
940    %3 = iconst.i64 2
941    %4 = ptr_add %1, %3
942    %5 = load.i32 %4, align 2
943    return %5
944",
945        );
946        let (module, stats) = run(&text);
947        assert!(!stats.changed());
948        assert_eq!(count_of(one(&module), Opcode::Load), 1);
949    }
950
951    #[test]
952    fn a_copy_the_oracle_could_not_tell_apart_from_the_load_is_not_followed() {
953        // The copy writes through one parameter and the load reads through another, so the two may
954        // be the same object and the oracle cannot say they are. Following that would be asking
955        // about the source of a copy that may not have written this at all, and a may is the
956        // reason to stop rather than a reason to carry on.
957        let text = wrap(
958            "(ptr, ptr, ptr) -> i32",
959            "block0(%0: ptr, %1: ptr, %2: ptr):
960    %3 = iconst.i32 7
961    store %3 -> %1, align 4
962    memcpy %0, %1, size 4, align 4
963    %4 = load.i32 %2, align 4
964    return %4
965",
966        );
967        let (module, stats) = run(&text);
968        assert!(!stats.changed());
969        assert_eq!(count_of(one(&module), Opcode::Load), 1);
970    }
971
972    #[test]
973    fn a_volatile_load_has_to_happen() {
974        let text = wrap(
975            "(ptr) -> i32",
976            "block0(%0: ptr):
977    %1 = iconst.i32 7
978    store %1 -> %0, align 4
979    %2 = load.i32.volatile %0, align 4
980    return %2
981",
982        );
983        let (module, stats) = run(&text);
984        assert!(!stats.changed());
985        assert_eq!(count_of(one(&module), Opcode::Load), 1);
986    }
987
988    #[test]
989    fn a_function_with_no_memory_in_it_is_left_alone() {
990        let text = wrap(
991            "(i32) -> i32",
992            "block0(%0: i32):
993    %1 = add %0, %0
994    return %1
995",
996        );
997        let (module, stats) = run(&text);
998        assert!(stats.is_empty(), "there was nothing here to say anything about");
999        off(one(&module));
1000    }
1001
1002    #[test]
1003    fn out_of_fuel_keeps_the_load_and_still_counts_it() {
1004        // The count of what could have gone is the same at every fuel setting, which is what
1005        // makes a bisection over it monotonic.
1006        let text = wrap(
1007            "(ptr) -> i32",
1008            "block0(%0: ptr):
1009    %1 = iconst.i32 7
1010    store %1 -> %0, align 4
1011    %2 = load.i32 %0, align 4
1012    return %2
1013",
1014        );
1015        let mut names = Interner::new();
1016        let mut module = parse(&text, &mut names).expect("the text parses");
1017        let id = module.funcs().next().expect("one function");
1018        let outside = Arc::new(Outside::of(&module));
1019        let mut an = crate::machine::fixtures::analyses().about(outside);
1020        let stats = RedundantLoad.run(&mut module[id], &mut an, &mut Fuel::of(0));
1021        assert!(!stats.changed());
1022        assert_eq!(stats.count(crate::stats::Kind::Missed, NO_FUEL), 1);
1023        off(&module[id]);
1024    }
1025
1026    #[test]
1027    fn what_the_walks_cost_is_written_down() {
1028        // Section 9.3 asks for the fraction that ran out of budget by name, and a number nothing
1029        // reports is a number nobody will look at.
1030        let text = wrap(
1031            "(ptr) -> i32",
1032            "block0(%0: ptr):
1033    %1 = iconst.i32 7
1034    store %1 -> %0, align 4
1035    %2 = load.i32 %0, align 4
1036    return %2
1037",
1038        );
1039        let (_, stats) = run(&text);
1040        assert_eq!(stats.count(crate::stats::Kind::Note, WALKS), 1);
1041        assert_eq!(stats.count(crate::stats::Kind::Note, STEPS), 1);
1042        assert_eq!(stats.count(crate::stats::Kind::Note, EXHAUSTED), 0);
1043    }
1044
1045    #[test]
1046    fn the_safety_instrumentation_between_them_does_not_stop_the_forward() {
1047        // What a safety build looks like by the time the optimizer sees it, which is the shape
1048        // above with the lifetime plane written and then read between the store and the load.
1049        // Both of those are on the memory chain and both have `%0` as an operand, so the walk
1050        // goes through them and has to be told they are not about `%0`.
1051        let text = wrap(
1052            "(ptr, i1) -> i32",
1053            "block0(%0: ptr, %1: i1):
1054    %2 = iconst.i32 7
1055    store %2 -> %0, align 4
1056    %3 = iconst.i64 4
1057    meta_init %0, %3
1058    br_if %1, block1, block2
1059
1060block1:
1061    %4 = cap_of %0
1062    check_bounds %4, %0, size 4, align 4
1063    jump block2
1064
1065block2:
1066    %5 = load.i32 %0, align 4
1067    return %5
1068",
1069        );
1070        let (module, stats) = run(&text);
1071        assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
1072        let func = one(&module);
1073        off(func);
1074        assert_eq!(count_of(func, Opcode::Load), 0);
1075        // And the instrumentation is still there, because this pass forwards loads and is not
1076        // entitled to an opinion about whether a check was worth running.
1077        assert_eq!(count_of(func, Opcode::MetaInit), 1);
1078        assert_eq!(count_of(func, Opcode::CheckBounds), 1);
1079    }
1080}