rucc_opt/dce.rs
1//! Dead code elimination: an instruction nothing uses and nothing depends on goes away.
2//!
3//! The other half of [`crate::fold`]. Folding rewrites an instruction in place and leaves its
4//! operands behind, used by nothing, so a function that folds well is a function whose printed IR
5//! grows a tail of arithmetic that computes numbers nobody reads. Every later pass will do the
6//! same thing, because a rewrite that has to clean up after itself is a rewrite that has to know
7//! what else was using what it replaced, and that is the knowledge this pass exists to hold in one
8//! place.
9//!
10//! It is not primarily an optimization. The backend materializes a constant where it is wanted
11//! rather than where the IR wrote it, so most of what this removes was already costing nothing in
12//! the output. What it buys is that a dump reads like the program, that the passes after it see a
13//! function whose size is the size of the work in it, and that a rule which fires on a dead
14//! instruction is a rule that fired on nothing rather than a rule that fired.
15//!
16//! # How it decides
17//!
18//! An instruction goes when it is not a terminator, when every value it produces is used by
19//! nothing, and when it does not happen for a reason of its own. [`Opcode::has_effects`] is the
20//! predicate for the last of those and it answers one question for two different things: it means
21//! both that an instruction writes memory or does something the program can observe, and that it
22//! reads memory. An allocation, a call and a `va_arg` are the first and stay. A plain load is only
23//! the second, and it goes.
24//!
25//! Removing a dead load needs no memory analysis, which is why it does not wait for one. It cannot
26//! change what any byte holds, it cannot change what another load sees, and nothing after it can
27//! tell that it did not happen. The only thing it changes is whether the program faults on an
28//! address it was never going to use the bytes of, and that is what a compiler is for. What does
29//! stay is a load the program asked to happen, which is a `volatile` one, and a load other threads
30//! can see the order of, which is an atomic one at any strength.
31//!
32//! An allocation nothing addresses is still removable and still here, and that one does want a
33//! memory analysis, because whether anything addresses it is the question.
34//!
35//! # Why it is a worklist
36//!
37//! Removing an instruction can kill the one that fed it, and that one can kill its own operand, so
38//! a single walk in any order finds a fraction of what is there. The counts are built once, and
39//! removing an instruction decrements what its operands were used for, and an operand that reaches
40//! zero puts its own definition back on the list. That reaches the same fixpoint a repeated walk
41//! would and touches each instruction about once.
42//!
43//! Uses are counted per occurrence rather than per instruction, because `x + x` uses `x` twice and
44//! removing one adder should not make `x` look dead.
45//!
46//! # What it does not remove
47//!
48//! Not a block parameter. A parameter nothing reads is dead in exactly the same sense, and taking
49//! it out means rewriting the argument list of every branch that arrives at the block, which is
50//! worth doing and is a different transformation from this one. The loop carried case is the
51//! interesting one there and it is the reason to do it separately: a parameter whose only use is
52//! the argument it passes to itself is dead, and seeing that needs the cycle broken rather than a
53//! count driven to zero.
54//!
55//! Not an unreachable block. A block no branch names is dead code by any definition, and removing
56//! it is control flow work rather than value work. It belongs with the branch folding that creates
57//! most of it.
58
59use rucc_ir::{Block, Def, Extra, Flags, Func, Inst, MemOrder, Opcode};
60
61use crate::uses::{count, operands};
62use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
63
64/// Recorded once for each instruction taken out.
65const REMOVED: &str = "instruction with no effects and no users removed";
66
67/// Recorded for an instruction that would have gone if there had been fuel for it.
68const NO_FUEL: &str = "dead instruction kept, the pass ran out of fuel";
69
70/// Recorded once for a function that has an instruction this pass is not allowed to look at.
71///
72/// The honest miss of this pass, and the one worth reading. A store nothing can read again, an
73/// allocation nothing addresses and a call that returns nothing and does nothing are all removable
74/// once there is a memory analysis to say so, and all of them stay. A function with none of these
75/// is a function where this pass found everything there was.
76const NEEDS_MEMORY_ANALYSIS: &str =
77 "instruction with effects left alone, removing it needs a memory analysis";
78
79/// The pass. It holds nothing, because the counts are per function and live in [`Pass::run`].
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct Dce;
82
83impl Pass for Dce {
84 fn name(&self) -> &'static str {
85 "dce"
86 }
87
88 fn describe(&self) -> &'static str {
89 "an instruction with no effects whose results nothing uses is removed"
90 }
91
92 fn preserves(&self) -> Preserved {
93 // Instructions go and blocks do not. A terminator is never dead, because it has an
94 // effect, so no block loses the thing that gives it its edges. What does go is a use, and
95 // the last use of a value is the end of its live range, so the liveness is not what it
96 // was and neither is anything counted off it. Nothing had caught this because no pass
97 // before this one in any pipeline builds the liveness, and an analysis nobody has built
98 // is an analysis nobody can be wrong about.
99 Preserved::ALL.without(Analysis::Liveness)
100 }
101
102 fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
103 let mut stats = Stats::new();
104 let mut uses = count(func);
105 let mut work: Vec<Inst> = Vec::new();
106 for block in func.blocks().collect::<Vec<Block>>() {
107 for inst in func.insts(block) {
108 match verdict(func, inst, &uses) {
109 Verdict::Dead => work.push(inst),
110 // Nothing reads it and it stays anyway, which is the one thing this pass
111 // gives up on rather than the thousands of instructions that are simply
112 // live. Counted here, in the one walk that sees every instruction, so the
113 // number is per function and not per visit of the worklist.
114 Verdict::Effects => stats.missed(NEEDS_MEMORY_ANALYSIS),
115 Verdict::Used | Verdict::Terminator => {}
116 }
117 }
118 }
119 while let Some(inst) = work.pop() {
120 // A worklist can name the same instruction twice, once from the first walk and once
121 // from an operand reaching zero, and the second visit finds it already gone.
122 if func.block_of(inst).is_none() {
123 continue;
124 }
125 if verdict(func, inst, &uses) != Verdict::Dead {
126 continue;
127 }
128 if !fuel.take() {
129 // Out of fuel, which stops the transforming and not the looking, the same way
130 // folding treats it. Draining the rest of the list without removing anything
131 // costs one pass over what is left and keeps the walk's shape independent of
132 // where the fuel ran out.
133 stats.missed(NO_FUEL);
134 continue;
135 }
136 operands(func, inst, |value| {
137 let count = &mut uses[value.index()];
138 *count -= 1;
139 if *count == 0 {
140 if let Def::Result { inst: def, .. } = func[value].def {
141 work.push(def);
142 }
143 }
144 });
145 func.remove_inst(inst);
146 stats.optimized(REMOVED);
147 }
148 stats
149 }
150}
151
152/// Whether this instruction can go, and when it cannot, what kept it.
153///
154/// The reason is separated out from the answer because two of the three reasons are ordinary and
155/// one of them is worth reporting. Nearly every instruction in a function is [`Verdict::Used`],
156/// which says nothing. [`Verdict::Effects`] is reached only by an instruction nothing reads, and
157/// there are few of those and every one of them is a thing this pass would take if it knew more.
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159enum Verdict {
160 /// Nothing reads it, nothing depends on it happening, and it can go.
161 Dead,
162 /// Something reads one of its results.
163 Used,
164 /// It ends a block, so the block goes with it or neither does.
165 Terminator,
166 /// Nothing reads it and it happens anyway, as far as this pass can tell.
167 Effects,
168}
169
170/// Whether this instruction only reads memory, so that not doing it is something nothing can tell.
171///
172/// A plain load and nothing else. A `volatile` load is an access the program asked for by name and
173/// happens whether or not anybody wanted the value. An atomic load is part of an order other
174/// threads can see, at every strength and not only at the fence-like ones, and there is no reason
175/// to argue about the weak end of that until something is waiting on the answer.
176fn reads_only(func: &Func, inst: Inst) -> bool {
177 let data = &func[inst];
178 if data.opcode != Opcode::Load || data.flags.contains(Flags::VOLATILE) {
179 return false;
180 }
181 let Extra::Mem(mem) = data.extra else { return false };
182 func[mem].order == MemOrder::NotAtomic
183}
184
185/// What to do with this instruction.
186fn verdict(func: &Func, inst: Inst, uses: &[u32]) -> Verdict {
187 let data = &func[inst];
188 // `is_terminator` on the function rather than on the opcode, because `asm goto` branches
189 // and its opcode does not say so. Inline assembly has effects either way, so this is belt
190 // and braces, and it is the cheaper of the two mistakes to make.
191 if func.is_terminator(inst) {
192 return Verdict::Terminator;
193 }
194 if !data.results().all(|value| uses[value.index()] == 0) {
195 return Verdict::Used;
196 }
197 if data.opcode.has_effects() && !reads_only(func, inst) {
198 return Verdict::Effects;
199 }
200 debug_assert!(
201 data.opcode != Opcode::InlineAsm,
202 "inline assembly has effects and cannot reach here"
203 );
204 Verdict::Dead
205}
206
207#[cfg(test)]
208mod tests {
209 use rucc_base::Interner;
210 use rucc_ir::{
211 Block, Builder, Flags, Func, MemInfo, MemOrder, Opcode, Restrict, Signature, Type,
212 };
213
214 use crate::stats::Kind;
215 use crate::{Analysis, Fuel, Pass, dce::Dce};
216
217 /// A function with one block, ready to have instructions appended to it.
218 fn blank() -> (Interner, Func, Block) {
219 let mut names = Interner::new();
220 let name = names.intern("f");
221 let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(32)]));
222 let block = func.create_block();
223 (names, func, block)
224 }
225
226 /// A four byte access of that strength, with nothing else said about it.
227 fn plain(order: MemOrder) -> MemInfo {
228 MemInfo { size: 4, align: 4, owns: 4, order, tbaa: None, restrict: Restrict::NONE }
229 }
230
231 /// How many instructions are left in a block.
232 fn left(func: &Func, block: Block) -> usize {
233 func.insts(block).count()
234 }
235
236 #[test]
237 fn arithmetic_nothing_reads_goes_away() {
238 let (_, mut func, block) = blank();
239 let mut build = Builder::new(&mut func, block);
240 let a = build.iconst(Type::int(32), 2);
241 let b = build.iconst(Type::int(32), 3);
242 build.binary(Opcode::Add, a, b, Flags::NONE);
243 build.ret(&[a]);
244 assert!(
245 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
246 .changed()
247 );
248 // The add, and then the constant that only it read. A single walk in this order would
249 // have removed the add and left the three behind, which is what the worklist is for.
250 assert_eq!(left(&func, block), 2);
251 }
252
253 #[test]
254 fn the_counts_in_the_cache_go_with_the_uses_that_were_removed() {
255 let (_, mut func, block) = blank();
256 let mut build = Builder::new(&mut func, block);
257 let a = build.iconst(Type::int(32), 2);
258 let b = build.iconst(Type::int(32), 3);
259 build.binary(Opcode::Add, a, b, Flags::NONE);
260 build.ret(&[a]);
261 let mut an = crate::machine::fixtures::analyses();
262 // Two values are live where the add is and one is live once it has gone, which is the
263 // fact this pass used to say it had left standing.
264 an.pressure(&func);
265 assert!(Dce.run(&mut func, &mut an, &mut Fuel::unlimited()).changed());
266 assert!(an.settle(&func, Dce.preserves(), true).is_empty(), "the pass was caught out");
267 assert!(!an.holds(Analysis::Pressure), "a stale count was left for the next pass to read");
268 }
269
270 #[test]
271 fn arithmetic_something_reads_stays() {
272 let (_, mut func, block) = blank();
273 let mut build = Builder::new(&mut func, block);
274 let a = build.iconst(Type::int(32), 2);
275 let b = build.iconst(Type::int(32), 3);
276 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
277 build.ret(&[sum]);
278 assert!(
279 !Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
280 .changed()
281 );
282 assert_eq!(left(&func, block), 4);
283 }
284
285 #[test]
286 fn a_value_used_twice_is_not_dead_when_one_use_goes() {
287 let (_, mut func, block) = blank();
288 let mut build = Builder::new(&mut func, block);
289 let x = build.iconst(Type::int(32), 7);
290 let kept = build.binary(Opcode::Add, x, x, Flags::NONE);
291 build.binary(Opcode::Add, x, x, Flags::NONE);
292 build.ret(&[kept]);
293 assert!(
294 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
295 .changed()
296 );
297 // Only the second add. Counting a use per instruction rather than per position would
298 // have driven the constant to zero and taken it out from under the first one.
299 assert_eq!(left(&func, block), 3);
300 }
301
302 #[test]
303 fn a_store_stays_however_dead_it_looks() {
304 let (_, mut func, block) = blank();
305 let mut build = Builder::new(&mut func, block);
306 let value = build.iconst(Type::int(32), 1);
307 let address = build.iconst(Type::int(64), 0);
308 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
309 let info = MemInfo {
310 size: 4,
311 align: 4,
312 order: MemOrder::NotAtomic,
313 tbaa: None,
314 owns: 0,
315 restrict: Restrict::NONE,
316 };
317 build.store(value, address, info, Flags::NONE);
318 build.ret(&[value]);
319 let stats =
320 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
321 assert!(!stats.changed());
322 assert_eq!(left(&func, block), 5);
323 // The store is the one instruction here that nothing reads and that stays anyway, so it
324 // is the one this pass reports as a miss. That count is the honest size of what a memory
325 // analysis would buy, per function, without anybody having to guess at it.
326 assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
327 }
328
329 /// A plain load nothing reads goes, which is the one thing here that does not wait for a
330 /// memory analysis. Removing it cannot change what any byte holds or what another load sees.
331 #[test]
332 fn a_load_nothing_reads_goes_away() {
333 let (_, mut func, block) = blank();
334 let mut build = Builder::new(&mut func, block);
335 let address = build.iconst(Type::int(64), 0);
336 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
337 build.load(Type::int(32), address, plain(MemOrder::NotAtomic), Flags::NONE);
338 let kept = build.iconst(Type::int(32), 1);
339 build.ret(&[kept]);
340 let stats =
341 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
342 assert!(stats.changed());
343 // The load, then the cast and the constant that only it read, so what is left is the
344 // constant the return reads and the return.
345 assert_eq!(left(&func, block), 2);
346 assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 0);
347 }
348
349 /// A `volatile` load stays. It is an access the program asked for by name, and it happens
350 /// whether or not anybody wanted the value it produced.
351 #[test]
352 fn a_volatile_load_nothing_reads_stays() {
353 let (_, mut func, block) = blank();
354 let mut build = Builder::new(&mut func, block);
355 let address = build.iconst(Type::int(64), 0);
356 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
357 build.load(Type::int(32), address, plain(MemOrder::NotAtomic), Flags::VOLATILE);
358 let kept = build.iconst(Type::int(32), 1);
359 build.ret(&[kept]);
360 let stats =
361 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
362 assert!(!stats.changed());
363 assert_eq!(left(&func, block), 5);
364 assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
365 }
366
367 /// An atomic load stays at every strength, because what it is part of is an order other
368 /// threads can see rather than the value it hands back.
369 #[test]
370 fn an_atomic_load_nothing_reads_stays_however_weak_it_is() {
371 for order in [MemOrder::Relaxed, MemOrder::Acquire, MemOrder::SeqCst] {
372 let (_, mut func, block) = blank();
373 let mut build = Builder::new(&mut func, block);
374 let address = build.iconst(Type::int(64), 0);
375 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
376 build.load(Type::int(32), address, plain(order), Flags::NONE);
377 let kept = build.iconst(Type::int(32), 1);
378 build.ret(&[kept]);
379 let stats = Dce.run(
380 &mut func,
381 &mut crate::machine::fixtures::analyses(),
382 &mut Fuel::unlimited(),
383 );
384 assert!(!stats.changed(), "{order:?}");
385 assert_eq!(left(&func, block), 5, "{order:?}");
386 }
387 }
388
389 #[test]
390 fn a_value_a_branch_passes_on_is_used_by_the_branch() {
391 let (_, mut func, block) = blank();
392 let target = func.create_block();
393 let param = func.append_param(target, Type::int(32));
394 let mut build = Builder::new(&mut func, block);
395 let x = build.iconst(Type::int(32), 9);
396 build.jump(target, &[x]);
397 let mut build = Builder::new(&mut func, target);
398 build.ret(&[param]);
399 assert!(
400 !Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
401 .changed()
402 );
403 // The constant is read by nothing in its own block and is not dead, because the only
404 // use an instruction can have that its argument list does not hold is this one.
405 assert_eq!(left(&func, block), 2);
406 }
407
408 #[test]
409 fn a_result_a_removed_instruction_read_is_looked_at_again() {
410 let (_, mut func, block) = blank();
411 let mut build = Builder::new(&mut func, block);
412 let a = build.iconst(Type::int(32), 2);
413 let b = build.iconst(Type::int(32), 3);
414 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
415 let doubled = build.binary(Opcode::Add, sum, sum, Flags::NONE);
416 build.unary(Opcode::SExt, doubled, Type::int(64));
417 let kept = build.iconst(Type::int(32), 1);
418 build.ret(&[kept]);
419 assert!(
420 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
421 .changed()
422 );
423 // A chain five long, dead from the far end, and all of it goes in one run. This is the
424 // case a walk in program order finds one instruction of per run.
425 assert_eq!(left(&func, block), 2);
426 }
427
428 #[test]
429 fn fuel_stops_the_removing_and_not_the_looking() {
430 let (_, mut func, block) = blank();
431 let mut build = Builder::new(&mut func, block);
432 let a = build.iconst(Type::int(32), 2);
433 let b = build.iconst(Type::int(32), 3);
434 build.binary(Opcode::Add, a, b, Flags::NONE);
435 build.ret(&[a]);
436 let mut fuel = Fuel::of(1);
437 let stats = Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut fuel);
438 assert!(stats.changed());
439 // The add and nothing after it, so the constant the add was keeping alive stays. One
440 // unit of fuel is one transformation, which is what makes a bisection over it land on
441 // a single site.
442 assert_eq!(left(&func, block), 3);
443 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
444 assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
445 }
446}