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