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(|¶m| !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}