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 { size: 0, align, order: MemOrder::NotAtomic, tbaa: None, restrict: Restrict::NONE }
268 }
269
270 /// An `alloca` of eight bytes, which is an address nothing outside the function knows.
271 fn local(build: &mut Builder<'_>) -> Value {
272 let mem = build.func().add_mem(MemInfo { size: 8, ..plain(8) });
273 build.value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR)
274 }
275
276 /// Runs the pass over the function with as much fuel as it wants.
277 fn run(func: &mut Func) -> Stats {
278 LoadForward.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
279 }
280
281 /// How many loads are left in the function.
282 fn loads(func: &Func) -> usize {
283 func.blocks()
284 .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
285 .filter(|&inst| func[inst].opcode == Opcode::Load)
286 .count()
287 }
288
289 /// What the return statement hands back, after the pass has pointed it somewhere.
290 fn returned(func: &Func) -> Vec<Value> {
291 let block = func.blocks().next().expect("the function has a block");
292 let inst = func.terminator(block).expect("the block has a terminator");
293 func[func[inst].args].to_vec()
294 }
295
296 #[test]
297 fn a_load_of_what_a_store_just_wrote_is_the_stored_value() {
298 let (_, mut func, block) = blank();
299 let mut build = Builder::new(&mut func, block);
300 let slot = local(&mut build);
301 let wrote = build.iconst(Type::int(64), 7);
302 build.store(wrote, slot, plain(8), Flags::NONE);
303 let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
304 build.ret(&[read]);
305
306 let stats = run(&mut func);
307 assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
308 assert_eq!(loads(&func), 0, "the load itself has to go, nothing else would remove it");
309 assert_eq!(returned(&func), vec![wrote]);
310 }
311
312 #[test]
313 fn the_second_load_of_an_address_is_what_the_first_one_read() {
314 let (_, mut func, block) = blank();
315 let mut build = Builder::new(&mut func, block);
316 let slot = local(&mut build);
317 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
318 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
319 let sum = build.binary(Opcode::Add, first, second, Flags::NONE);
320 build.ret(&[sum]);
321
322 let stats = run(&mut func);
323 assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
324 assert_eq!(loads(&func), 1, "one read of that address has to happen and only one");
325 let sum = returned(&func)[0];
326 let rucc_ir::Def::Result { inst, .. } = func[sum].def else { panic!("the add is gone") };
327 assert_eq!(func[func[inst].args].to_vec(), vec![first, first]);
328 }
329
330 #[test]
331 fn a_store_of_a_different_width_is_not_forwarded_through() {
332 let (_, mut func, block) = blank();
333 let mut build = Builder::new(&mut func, block);
334 let slot = local(&mut build);
335 let wrote = build.iconst(Type::int(32), 7);
336 build.store(wrote, slot, plain(4), Flags::NONE);
337 let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
338 build.ret(&[read]);
339
340 let stats = run(&mut func);
341 assert_eq!(stats.count(crate::stats::Kind::Missed, WIDTH), 1);
342 assert_eq!(loads(&func), 1, "a four byte store does not say what eight bytes hold");
343 assert_eq!(returned(&func), vec![read]);
344 }
345
346 #[test]
347 fn a_call_between_the_two_accesses_is_a_write_to_everything() {
348 let (mut names, mut func, block) = blank();
349 let mut build = Builder::new(&mut func, block);
350 let slot = local(&mut build);
351 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
352 let signature = build.func().add_signature(Signature::new());
353 build.call(names.intern("g"), signature, &[]);
354 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
355 build.ret(&[first, second]);
356
357 let stats = run(&mut func);
358 assert!(!stats.changed(), "nothing here says what the call did to that address");
359 assert_eq!(loads(&func), 2);
360 }
361
362 #[test]
363 fn a_store_to_another_address_is_a_write_to_everything_too() {
364 let (_, mut func, block) = blank();
365 let mut build = Builder::new(&mut func, block);
366 let slot = local(&mut build);
367 let other = local(&mut build);
368 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
369 build.store(first, other, plain(8), Flags::NONE);
370 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
371 build.ret(&[first, second]);
372
373 // Two allocas cannot be the same object, so this is an opportunity and not a hazard. It is
374 // left on the table on purpose: telling the two apart is the alias oracle's answer and
375 // this pass is the version that does not have one.
376 let stats = run(&mut func);
377 assert!(!stats.changed());
378 assert_eq!(loads(&func), 2);
379 }
380
381 #[test]
382 fn a_volatile_load_is_not_reused_and_nothing_before_it_survives_it() {
383 let (_, mut func, block) = blank();
384 let mut build = Builder::new(&mut func, block);
385 let slot = local(&mut build);
386 let first = build.load(Type::int(64), slot, plain(8), Flags::VOLATILE);
387 let second = build.load(Type::int(64), slot, plain(8), Flags::VOLATILE);
388 let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
389 build.ret(&[first, second, third]);
390
391 let stats = run(&mut func);
392 assert!(!stats.changed(), "every volatile read has to happen");
393 assert_eq!(loads(&func), 3);
394 }
395
396 #[test]
397 fn what_one_block_knows_does_not_reach_the_next_one() {
398 let (_, mut func, entry) = blank();
399 let next = func.create_block();
400 let mut build = Builder::new(&mut func, entry);
401 let slot = local(&mut build);
402 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
403 build.jump(next, &[]);
404 let mut build = Builder::new(&mut func, next);
405 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
406 build.ret(&[first, second]);
407
408 // The value is available and the `-O2` version of this pass finds it. Section 16.2 is
409 // explicit that this one does not, because reaching it means memory SSA.
410 let stats = run(&mut func);
411 assert!(!stats.changed());
412 assert_eq!(loads(&func), 2);
413 }
414
415 #[test]
416 fn a_chain_of_reads_all_come_from_the_first_one() {
417 let (_, mut func, block) = blank();
418 let mut build = Builder::new(&mut func, block);
419 let slot = local(&mut build);
420 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
421 build.load(Type::int(64), slot, plain(8), Flags::NONE);
422 let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
423 build.ret(&[third]);
424
425 let stats = run(&mut func);
426 assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 2);
427 assert_eq!(loads(&func), 1);
428 assert_eq!(returned(&func), vec![first]);
429 }
430
431 #[test]
432 fn a_store_of_a_value_the_pass_is_removing_forwards_to_where_that_value_went() {
433 let (_, mut func, block) = blank();
434 let mut build = Builder::new(&mut func, block);
435 let slot = local(&mut build);
436 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
437 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
438 build.store(second, slot, plain(8), Flags::NONE);
439 let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
440 build.ret(&[third]);
441
442 // The store writes a value this pass is in the middle of taking away, so the third load
443 // has to land one step further back than what the table says. Landing on the second load's
444 // result would leave the function reading an instruction that is no longer in it.
445 let stats = run(&mut func);
446 assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
447 assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
448 assert_eq!(loads(&func), 1);
449 assert_eq!(returned(&func), vec![first]);
450 }
451
452 #[test]
453 fn without_fuel_the_load_stays_and_the_chance_is_still_counted() {
454 let (_, mut func, block) = blank();
455 let mut build = Builder::new(&mut func, block);
456 let slot = local(&mut build);
457 let wrote = build.iconst(Type::int(64), 7);
458 build.store(wrote, slot, plain(8), Flags::NONE);
459 let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
460 build.ret(&[read]);
461
462 let stats =
463 LoadForward.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::of(0));
464 assert!(!stats.changed());
465 assert_eq!(stats.count(crate::stats::Kind::Missed, NO_FUEL), 1);
466 assert_eq!(loads(&func), 1);
467 }
468}