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