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, and what that means is the alias oracle's answer. A call, a
43//! store and a safety plane access each take out the entries the oracle says they may write and
44//! leave the rest standing. Everything else that touches memory, which is an atomic, a fence, a
45//! `memcpy` and anything else [`Opcode::touches_memory`] is true of, empties the table and records
46//! nothing. That predicate is the conservative one, so an opcode added to the IR later throws the
47//! table away rather than being quietly assumed harmless.
48//!
49//! The plane access is there because of what it costs to leave it out. On a build with
50//! `-fsafety=detect` there is a `meta_` or a `check_` beside almost every access in the program, so
51//! a pass that empties the table at each of them has an empty table almost all of the time. None of
52//! them is a write to the address it names, which is what [`Opcode::touches_only_planes`] says and
53//! what the oracle now answers with.
54//!
55//! The oracle arrived late and this pass is the first consumer it has ever had. Until tamnd/rucc#1467
56//! a call and a store both emptied the whole table, because `crate::alias` wanted the module and a
57//! pass is handed one function. What the whole table cost was the second address: `*p = v; total +=
58//! *q;` with two locals was refused even where the two cannot be the same object. It is not refused
59//! now.
60//!
61//! What the oracle is worth here rests on the escape analysis more than on anything else in it.
62//! `spec/optimizer/08-alias-analysis.md` section 8.4 calls that the cheapest interprocedural
63//! flavoured fact there is, and it is what answers the ordinary case: a local whose address never
64//! leaves the function cannot be touched by any call in it, whatever the callee does.
65//!
66//! A volatile access empties the table and records nothing either way. Whether a volatile store
67//! could be forwarded from is an argument about what `volatile` promises, and this pass does not
68//! need to have it.
69//!
70//! # The width, which is where the miscompilation would be
71//!
72//! Section 16.6 names it as the single most likely wrong answer in that document: a load forwarded
73//! from a store of a different size. Section 09.5 has the three-way distinction, which is that a
74//! store covering the load exactly is the value, one covering it partially needs an extract, and
75//! one not covering it at all means the walk should continue.
76//!
77//! This pass only ever takes the first of the three. The address has to be the same SSA value and
78//! the type has to be equal, which is the same width and the same reading of the bits, and
79//! anything else is left alone and counted. Two-way is what somebody writes first and it is right
80//! most of the time, which is what makes it worth being explicit that this is not that.
81//!
82//! # What it leaves behind
83//!
84//! The load goes, rather than staying and having its result forwarded. [`crate::dce`] would not
85//! remove it: `has_effects` is true of every load, because a pass that removed one would need to
86//! know the address is dereferenced anyway, and this is the pass that knows it. The load being
87//! removed is safe for a reason nothing else in the pipeline has: something already read or wrote
88//! that exact address in this block, so the address is one the program dereferences whatever
89//! happens next.
90//!
91//! The store stays. Removing a store that a later store covers is dead store elimination, which is
92//! document 17 and a different pass.
93
94use std::collections::HashMap;
95
96use rucc_ir::{Block, Flags, Func, Inst, Opcode, Type, Value};
97
98use crate::alias::{Access, Alias};
99use crate::uses::substitute;
100use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
101
102/// What this pass is called, which the pipeline matches on to decide whether to build the module
103/// facts the oracle asks for.
104pub const NAME: &str = "load-forward";
105
106/// Recorded for a load that read what a store in the same block had just written.
107const FORWARDED: &str = "load replaced by the value a store in the same block wrote there";
108
109/// Recorded for a load of an address an earlier load in the same block had already read.
110const REUSED: &str = "load replaced by what an earlier load of the same address read";
111
112/// Recorded for a load whose address is known and whose type is not the type it is known at.
113const WIDTH: &str = "load kept, what is known about that address is a different type";
114
115/// Recorded for a load that would have gone if there had been fuel for it.
116const NO_FUEL: &str = "redundant load kept, the pass ran out of fuel";
117
118/// The pass.
119#[derive(Debug)]
120pub struct LoadForward;
121
122impl Pass for LoadForward {
123 fn name(&self) -> &'static str {
124 "load-forward"
125 }
126
127 fn describe(&self) -> &'static str {
128 "a load of an address the block has already read or written is that value"
129 }
130
131 fn preserves(&self) -> Preserved {
132 // The shape of the function. No block is added, none is removed, no edge moves, and the
133 // instructions that go are loads, which are never terminators.
134 //
135 // The liveness is the one thing that does move, for the reason `crate::simplify` gives:
136 // pointing every reader of one value at another is one more place the second is live and
137 // one fewer the first is.
138 Preserved::ALL.without(Analysis::Liveness)
139 }
140
141 fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
142 let mut stats = Stats::new();
143 // What each removed load's result is read as, applied to the whole function once at the
144 // end. Rewriting each one where it is found would be a walk over the function per load,
145 // and there is nothing to gain by it: what this pass looks at is the address, and a
146 // redirection of a result does not change one.
147 let mut forward: HashMap<Value, Value> = HashMap::new();
148 let mut gone: Vec<Inst> = Vec::new();
149
150 // The oracle borrows the function, so the walk that reads it is a scope of its own and
151 // every edit happens after it. Built once, because the escape analysis inside it is one
152 // walk over the function and every query may ask it.
153 {
154 let mut alias = Alias::new(func, an.outside()).knowing(an.modref());
155 for block in func.blocks().collect::<Vec<Block>>() {
156 let mut known: HashMap<Value, Held> = HashMap::new();
157 for inst in func.insts(block).collect::<Vec<Inst>>() {
158 match act(func, inst) {
159 Act::Ignore => {}
160 Act::Forget => known.clear(),
161 Act::Ask => {
162 known.retain(|_, held| alias.clobbered_by(&held.access, inst).is_no());
163 }
164 Act::Wrote { address, value, ty } => {
165 // The entries this store may have written go, and the rest stay. Its
166 // own goes in after, so that the address it just wrote survives its own
167 // clearing whatever the oracle made of it.
168 let Some(wrote) = alias.writes(inst) else {
169 known.clear();
170 continue;
171 };
172 known.retain(|_, held| alias.query(&held.access, &wrote).is_no());
173 known.insert(address, Held { ty, value, stored: true, access: wrote });
174 }
175 Act::Read { address, result, ty } => {
176 match known.get(&address).copied() {
177 Some(held) if held.ty == ty => {
178 if fuel.take() {
179 forward.insert(result, held.value);
180 gone.push(inst);
181 stats.optimized(if held.stored {
182 FORWARDED
183 } else {
184 REUSED
185 });
186 continue;
187 }
188 // Out of fuel, which is a request to stop transforming and not
189 // to stop looking. The walk goes on so that the count of what
190 // could have gone is the same at every fuel setting, which is
191 // what makes a bisection over it monotonic.
192 stats.missed(NO_FUEL);
193 }
194 Some(_) => stats.missed(WIDTH),
195 None => {}
196 }
197 // A load with no access is a load the oracle could say nothing about
198 // later, so it is not recorded at all rather than recorded as
199 // something no call can be asked about.
200 if let Some(read) = alias.reads(inst) {
201 known.insert(
202 address,
203 Held { ty, value: result, stored: false, access: read },
204 );
205 }
206 }
207 }
208 }
209 }
210 }
211
212 for inst in gone {
213 func.remove_inst(inst);
214 }
215 if !forward.is_empty() {
216 substitute(func, &forward);
217 }
218 stats
219 }
220}
221
222/// What the block knows about one address.
223///
224/// The value and the type are what the forwarding turns on. Whether it was stored or read is only
225/// for the counters, and they want it because the two numbers answer different questions. A
226/// forward from a store says the program wrote something and read it straight back, which is what
227/// an unrolled loop over an array looks like and what the constant folder can usually finish off.
228/// A reuse says the program read the same place twice, which is the `p->x` this pass is named for
229/// and which folds into nothing at all.
230#[derive(Clone, Copy)]
231struct Held {
232 /// The type the address is known at, which a load has to match exactly to be that value.
233 ty: Type,
234 /// What the address holds.
235 value: Value,
236 /// Whether a store put it there, rather than a load having read it.
237 stored: bool,
238 /// Which bytes it is, for asking the oracle whether a call or a store reaches them.
239 ///
240 /// The access of the instruction that put the entry here, which is the same address and the
241 /// same width as any load that will match it, since matching is by address value and by type.
242 access: Access,
243}
244
245/// What one instruction does to the table.
246enum Act {
247 /// Nothing. It touches no memory.
248 Ignore,
249 /// It could write anywhere the oracle cannot rule out, and nothing here says what it wrote.
250 Forget,
251 /// The oracle is asked what it wrote, because nothing about its shape says.
252 Ask,
253 /// It writes this value of this type at this address, and may write elsewhere.
254 Wrote { address: Value, value: Value, ty: Type },
255 /// It reads a value of this type from this address into this result.
256 Read { address: Value, result: Value, ty: Type },
257}
258
259/// Which of the five an instruction is.
260///
261/// The two interesting cases are narrow on purpose. A plain non-volatile `Load` with one address
262/// and one result, and a plain non-volatile `Store` of one value to one address. `AtomicLoad` and
263/// `AtomicStore` are separate opcodes in this IR and are not these, so an ordering never reaches
264/// here as something to forward, and neither does a load carrying a memory token, which is what
265/// more than one result would mean.
266///
267/// `Ask` is what an instruction that writes memory without an access saying where gets, and the
268/// oracle is asked a different question about one of those: `Alias::clobbered_by` rather than
269/// `Alias::query`, since there is no access of its own to hand over. A call is the obvious member
270/// and what it may write is what its attributes and its arguments say. The safety instrumentation
271/// is the other one, and there the answer is about the opcode rather than about the callee: a
272/// plane write is not a write to the address it names, which is what [`Opcode::touches_only_planes`]
273/// is for. Sending those to `Forget` instead is what this pass used to do, and it meant that on a
274/// build with `-fsafety=detect` the table was emptied beside almost every access and the pass did
275/// close to nothing, which is tamnd/rucc#1501.
276fn act(func: &Func, inst: Inst) -> Act {
277 let data = &func[inst];
278 if !data.opcode.touches_memory() {
279 return Act::Ignore;
280 }
281 if data.flags.contains(Flags::VOLATILE) {
282 return Act::Forget;
283 }
284 let args = &func[data.args];
285 match data.opcode {
286 Opcode::Call | Opcode::CallIndirect | Opcode::TailCall => Act::Ask,
287 other if other.touches_only_planes() => Act::Ask,
288 Opcode::Load => {
289 let mut results = data.results();
290 let (Some(&address), Some(result), None) =
291 (args.first(), results.next(), results.next())
292 else {
293 return Act::Forget;
294 };
295 Act::Read { address, result, ty: func[result].ty }
296 }
297 Opcode::Store => {
298 let (Some(&value), Some(&address)) = (args.first(), args.get(1)) else {
299 return Act::Forget;
300 };
301 Act::Wrote { address, value, ty: func[value].ty }
302 }
303 _ => Act::Forget,
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use std::sync::Arc;
310
311 use rucc_base::Interner;
312 use rucc_ir::{
313 AttrSet, Attrs, Block, Builder, Extra, Flags, Func, InstData, MemInfo, MemOrder, Module,
314 Restrict, Signature, Type, Value,
315 };
316 use rucc_target::{TargetInfo, Triple};
317
318 use super::*;
319 use crate::Fuel;
320 use crate::outside::Outside;
321
322 /// An empty function with one block, which is where every test below builds.
323 fn blank() -> (Interner, Func, Block) {
324 let mut names = Interner::new();
325 let name = names.intern("f");
326 let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(64)]));
327 let block = func.create_block();
328 (names, func, block)
329 }
330
331 /// An ordinary access of that alignment, with nothing said about its type.
332 fn plain(align: u32) -> MemInfo {
333 MemInfo {
334 size: 0,
335 align,
336 order: MemOrder::NotAtomic,
337 tbaa: None,
338 owns: 0,
339 restrict: Restrict::NONE,
340 }
341 }
342
343 /// An `alloca` of eight bytes, which is an address nothing outside the function knows.
344 fn local(build: &mut Builder<'_>) -> Value {
345 let mem = build.func().add_mem(MemInfo { size: 8, ..plain(8) });
346 build.value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR)
347 }
348
349 /// Runs the pass over the function with as much fuel as it wants and no module facts.
350 fn run(func: &mut Func) -> Stats {
351 LoadForward.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
352 }
353
354 /// The same, with what the oracle would know if this function were in that module.
355 fn run_in(func: &mut Func, module: &Module) -> Stats {
356 let mut an = crate::machine::fixtures::analyses().about(Arc::new(Outside::of(module)));
357 LoadForward.run(func, &mut an, &mut Fuel::unlimited())
358 }
359
360 /// How many loads are left in the function.
361 fn loads(func: &Func) -> usize {
362 func.blocks()
363 .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
364 .filter(|&inst| func[inst].opcode == Opcode::Load)
365 .count()
366 }
367
368 /// What the return statement hands back, after the pass has pointed it somewhere.
369 fn returned(func: &Func) -> Vec<Value> {
370 let block = func.blocks().next().expect("the function has a block");
371 let inst = func.terminator(block).expect("the block has a terminator");
372 func[func[inst].args].to_vec()
373 }
374
375 #[test]
376 fn a_load_of_what_a_store_just_wrote_is_the_stored_value() {
377 let (_, mut func, block) = blank();
378 let mut build = Builder::new(&mut func, block);
379 let slot = local(&mut build);
380 let wrote = build.iconst(Type::int(64), 7);
381 build.store(wrote, slot, plain(8), Flags::NONE);
382 let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
383 build.ret(&[read]);
384
385 let stats = run(&mut func);
386 assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
387 assert_eq!(loads(&func), 0, "the load itself has to go, nothing else would remove it");
388 assert_eq!(returned(&func), vec![wrote]);
389 }
390
391 #[test]
392 fn the_second_load_of_an_address_is_what_the_first_one_read() {
393 let (_, mut func, block) = blank();
394 let mut build = Builder::new(&mut func, block);
395 let slot = local(&mut build);
396 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
397 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
398 let sum = build.binary(Opcode::Add, first, second, Flags::NONE);
399 build.ret(&[sum]);
400
401 let stats = run(&mut func);
402 assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
403 assert_eq!(loads(&func), 1, "one read of that address has to happen and only one");
404 let sum = returned(&func)[0];
405 let rucc_ir::Def::Result { inst, .. } = func[sum].def else { panic!("the add is gone") };
406 assert_eq!(func[func[inst].args].to_vec(), vec![first, first]);
407 }
408
409 #[test]
410 fn a_store_of_a_different_width_is_not_forwarded_through() {
411 let (_, mut func, block) = blank();
412 let mut build = Builder::new(&mut func, block);
413 let slot = local(&mut build);
414 let wrote = build.iconst(Type::int(32), 7);
415 build.store(wrote, slot, plain(4), Flags::NONE);
416 let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
417 build.ret(&[read]);
418
419 let stats = run(&mut func);
420 assert_eq!(stats.count(crate::stats::Kind::Missed, WIDTH), 1);
421 assert_eq!(loads(&func), 1, "a four byte store does not say what eight bytes hold");
422 assert_eq!(returned(&func), vec![read]);
423 }
424
425 #[test]
426 fn a_call_cannot_touch_a_local_whose_address_never_left_the_function() {
427 let (mut names, mut func, block) = blank();
428 let mut build = Builder::new(&mut func, block);
429 let slot = local(&mut build);
430 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
431 let signature = build.func().add_signature(Signature::new());
432 build.call(names.intern("g"), signature, &[]);
433 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
434 build.ret(&[first, second]);
435
436 // The escape analysis and nothing else. `g` is a name this module has never heard of and
437 // it could do anything at all, and it still cannot reach an address it was never given.
438 let stats = run(&mut func);
439 assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
440 assert_eq!(loads(&func), 1);
441 assert_eq!(returned(&func), vec![first, first]);
442 }
443
444 #[test]
445 fn a_call_handed_the_address_is_a_write_to_it() {
446 let (mut names, mut func, block) = blank();
447 let mut build = Builder::new(&mut func, block);
448 let slot = local(&mut build);
449 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
450 let signature = build.func().add_signature(Signature::new().with_params(&[Type::PTR]));
451 build.call(names.intern("g"), signature, &[slot]);
452 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
453 build.ret(&[first, second]);
454
455 // The address escaped into the call, so the callee has it and nothing here says what it
456 // did with it. This is the half of the previous test that must not move.
457 let stats = run(&mut func);
458 assert!(!stats.changed());
459 assert_eq!(loads(&func), 2);
460 }
461
462 #[test]
463 fn a_store_to_an_address_that_cannot_be_this_one_leaves_it_alone() {
464 let (_, mut func, block) = blank();
465 let mut build = Builder::new(&mut func, block);
466 let slot = local(&mut build);
467 let other = local(&mut build);
468 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
469 build.store(first, other, plain(8), Flags::NONE);
470 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
471 build.ret(&[first, second]);
472
473 // Two allocas cannot be the same object, which is the oracle's first layer and the one
474 // that answers most of what real code asks. Until tamnd/rucc#1467 this was left on the
475 // table with a comment saying so.
476 let stats = run(&mut func);
477 assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
478 assert_eq!(loads(&func), 1);
479 assert_eq!(returned(&func), vec![first, first]);
480 }
481
482 #[test]
483 fn a_store_the_oracle_cannot_place_takes_the_table_with_it() {
484 let (mut names, mut func, entry) = blank();
485 let elsewhere = func.append_param(entry, Type::PTR);
486 let mut build = Builder::new(&mut func, entry);
487 let slot = local(&mut build);
488 // Handed out, so the local is one somebody else's pointer could be naming.
489 let signature = build.func().add_signature(Signature::new().with_params(&[Type::PTR]));
490 build.call(names.intern("g"), signature, &[slot]);
491 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
492 build.store(first, elsewhere, plain(8), Flags::NONE);
493 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
494 build.ret(&[first, second]);
495
496 // A pointer handed in is an address the walk cannot follow back to an object, and this
497 // local's address did leave the function, so the escape layer has nothing to say either.
498 // Nothing left says these are two objects, so the store may be to this one.
499 let stats = run(&mut func);
500 assert!(!stats.changed());
501 assert_eq!(loads(&func), 2);
502 }
503
504 #[test]
505 fn a_store_through_a_pointer_from_nowhere_cannot_reach_a_local_that_stayed_here() {
506 let (_, mut func, entry) = blank();
507 let elsewhere = func.append_param(entry, Type::PTR);
508 let mut build = Builder::new(&mut func, entry);
509 let slot = local(&mut build);
510 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
511 build.store(first, elsewhere, plain(8), Flags::NONE);
512 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
513 build.ret(&[first, second]);
514
515 // The other half of the test above. Nothing ever handed this address out, so no pointer
516 // this function cannot follow is naming it, whatever that pointer is.
517 let stats = run(&mut func);
518 assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
519 assert_eq!(loads(&func), 1);
520 }
521
522 #[test]
523 fn a_volatile_load_is_not_reused_and_nothing_before_it_survives_it() {
524 let (_, mut func, block) = blank();
525 let mut build = Builder::new(&mut func, block);
526 let slot = local(&mut build);
527 let first = build.load(Type::int(64), slot, plain(8), Flags::VOLATILE);
528 let second = build.load(Type::int(64), slot, plain(8), Flags::VOLATILE);
529 let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
530 build.ret(&[first, second, third]);
531
532 let stats = run(&mut func);
533 assert!(!stats.changed(), "every volatile read has to happen");
534 assert_eq!(loads(&func), 3);
535 }
536
537 #[test]
538 fn the_safety_instrumentation_between_two_reads_leaves_the_table_standing() {
539 // A safety build puts a plane write and then a plane read beside almost every access, and
540 // for as long as those emptied the table this pass did close to nothing at
541 // `-fsafety=detect`. Neither is a write to the address it names, so the second load is
542 // still the first one's value.
543 let (_, mut func, block) = blank();
544 let mut build = Builder::new(&mut func, block);
545 let slot = local(&mut build);
546 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
547 let width = build.iconst(Type::int(64), 8);
548 let args = build.func().push_values(&[slot, width]);
549 build.inst(InstData { args, ..InstData::new(Opcode::MetaInit) }, &[]);
550 let args = build.func().push_values(&[slot]);
551 let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
552 let args = build.func().push_values(&[capability, slot]);
553 build.inst(InstData { args, ..InstData::new(Opcode::CheckLive) }, &[]);
554 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
555 build.ret(&[first, second]);
556
557 let stats = run(&mut func);
558 assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
559 assert_eq!(loads(&func), 1);
560 assert_eq!(returned(&func), vec![first, first]);
561 }
562
563 #[test]
564 fn a_fence_between_two_reads_still_empties_the_table() {
565 // The other side of the line above. A fence touches memory, says nothing about where, and
566 // is not one of the plane opcodes, so it falls where everything unrecognized falls.
567 let (_, mut func, block) = blank();
568 let mut build = Builder::new(&mut func, block);
569 let slot = local(&mut build);
570 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
571 build.inst(InstData::new(Opcode::Fence), &[]);
572 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
573 build.ret(&[first, second]);
574
575 let stats = run(&mut func);
576 assert!(!stats.changed());
577 assert_eq!(loads(&func), 2);
578 }
579
580 #[test]
581 fn a_volatile_plane_access_is_read_as_volatile_first() {
582 // The volatile test comes before the opcode does, and it has to stay that way: an access
583 // the program asked to happen exactly as written happens, whatever the opcode would have
584 // said about which memory it is.
585 let (_, mut func, block) = blank();
586 let mut build = Builder::new(&mut func, block);
587 let slot = local(&mut build);
588 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
589 let width = build.iconst(Type::int(64), 8);
590 let args = build.func().push_values(&[slot, width]);
591 build.inst(
592 InstData { args, flags: Flags::VOLATILE, ..InstData::new(Opcode::MetaInit) },
593 &[],
594 );
595 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
596 build.ret(&[first, second]);
597
598 let stats = run(&mut func);
599 assert!(!stats.changed());
600 assert_eq!(loads(&func), 2);
601 }
602
603 #[test]
604 fn what_one_block_knows_does_not_reach_the_next_one() {
605 let (_, mut func, entry) = blank();
606 let next = func.create_block();
607 let mut build = Builder::new(&mut func, entry);
608 let slot = local(&mut build);
609 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
610 build.jump(next, &[]);
611 let mut build = Builder::new(&mut func, next);
612 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
613 build.ret(&[first, second]);
614
615 // The value is available and the `-O2` version of this pass finds it. Section 16.2 is
616 // explicit that this one does not, because reaching it means memory SSA.
617 let stats = run(&mut func);
618 assert!(!stats.changed());
619 assert_eq!(loads(&func), 2);
620 }
621
622 #[test]
623 fn a_chain_of_reads_all_come_from_the_first_one() {
624 let (_, mut func, block) = blank();
625 let mut build = Builder::new(&mut func, block);
626 let slot = local(&mut build);
627 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
628 build.load(Type::int(64), slot, plain(8), Flags::NONE);
629 let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
630 build.ret(&[third]);
631
632 let stats = run(&mut func);
633 assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 2);
634 assert_eq!(loads(&func), 1);
635 assert_eq!(returned(&func), vec![first]);
636 }
637
638 #[test]
639 fn a_store_of_a_value_the_pass_is_removing_forwards_to_where_that_value_went() {
640 let (_, mut func, block) = blank();
641 let mut build = Builder::new(&mut func, block);
642 let slot = local(&mut build);
643 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
644 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
645 build.store(second, slot, plain(8), Flags::NONE);
646 let third = build.load(Type::int(64), slot, plain(8), Flags::NONE);
647 build.ret(&[third]);
648
649 // The store writes a value this pass is in the middle of taking away, so the third load
650 // has to land one step further back than what the table says. Landing on the second load's
651 // result would leave the function reading an instruction that is no longer in it.
652 let stats = run(&mut func);
653 assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
654 assert_eq!(stats.count(crate::stats::Kind::Optimized, FORWARDED), 1);
655 assert_eq!(loads(&func), 1);
656 assert_eq!(returned(&func), vec![first]);
657 }
658
659 #[test]
660 fn a_call_to_something_declared_to_read_no_memory_writes_none_either() {
661 // The module's half of the oracle, which is the one thing a pass handed a function cannot
662 // work out for itself. The address escaped into an earlier call, so the escape layer has
663 // nothing left to say and what answers is `g` being declared `const`.
664 let mut names = Interner::new();
665 let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
666 let mut module = Module::new(names.intern("t.c"), &target);
667 let quiet = names.intern("g");
668 let mut callee = Func::new(quiet, Signature::new().with_params(&[Type::PTR]));
669 callee.attrs = Attrs { set: AttrSet::READNONE, ..Attrs::default() };
670 module.add_func(callee);
671
672 let name = names.intern("f");
673 let build = || {
674 let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(64)]));
675 let block = func.create_block();
676 let mut build = Builder::new(&mut func, block);
677 let slot = local(&mut build);
678 let signature = build.func().add_signature(Signature::new().with_params(&[Type::PTR]));
679 build.call(quiet, signature, &[slot]);
680 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
681 build.call(quiet, signature, &[slot]);
682 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
683 build.ret(&[first, second]);
684 func
685 };
686
687 assert!(!run(&mut build()).changed(), "without the module there is nothing to read");
688 let mut func = build();
689 let stats = run_in(&mut func, &module);
690 assert_eq!(stats.count(crate::stats::Kind::Optimized, REUSED), 1);
691 assert_eq!(loads(&func), 1);
692 }
693
694 #[test]
695 fn without_fuel_the_load_stays_and_the_chance_is_still_counted() {
696 let (_, mut func, block) = blank();
697 let mut build = Builder::new(&mut func, block);
698 let slot = local(&mut build);
699 let wrote = build.iconst(Type::int(64), 7);
700 build.store(wrote, slot, plain(8), Flags::NONE);
701 let read = build.load(Type::int(64), slot, plain(8), Flags::NONE);
702 build.ret(&[read]);
703
704 let stats =
705 LoadForward.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::of(0));
706 assert!(!stats.changed());
707 assert_eq!(stats.count(crate::stats::Kind::Missed, NO_FUEL), 1);
708 assert_eq!(loads(&func), 1);
709 }
710}