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 [`Opcode::has_effects`] says no, and when
19//! every value it produces is used by nothing. All three are needed and the second is where the
20//! argument lives: `has_effects` is the conservative predicate, so a load, an allocation, a call
21//! and a `va_arg` all stay whatever their results do. That is stricter than it has to be, since a
22//! non-volatile load of a dead value is safe to remove and so is an allocation nothing addresses,
23//! but both of those want memory analysis to say so honestly and this pass predates it.
24//!
25//! # Why it is a worklist
26//!
27//! Removing an instruction can kill the one that fed it, and that one can kill its own operand, so
28//! a single walk in any order finds a fraction of what is there. The counts are built once, and
29//! removing an instruction decrements what its operands were used for, and an operand that reaches
30//! zero puts its own definition back on the list. That reaches the same fixpoint a repeated walk
31//! would and touches each instruction about once.
32//!
33//! Uses are counted per occurrence rather than per instruction, because `x + x` uses `x` twice and
34//! removing one adder should not make `x` look dead.
35//!
36//! # What it does not remove
37//!
38//! Not a block parameter. A parameter nothing reads is dead in exactly the same sense, and taking
39//! it out means rewriting the argument list of every branch that arrives at the block, which is
40//! worth doing and is a different transformation from this one. The loop carried case is the
41//! interesting one there and it is the reason to do it separately: a parameter whose only use is
42//! the argument it passes to itself is dead, and seeing that needs the cycle broken rather than a
43//! count driven to zero.
44//!
45//! Not an unreachable block. A block no branch names is dead code by any definition, and removing
46//! it is control flow work rather than value work. It belongs with the branch folding that creates
47//! most of it.
48
49use rucc_ir::{Block, Def, Func, Inst, Opcode};
50
51use crate::uses::{count, operands};
52use crate::{Analyses, Fuel, Pass, Preserved, Stats};
53
54/// Recorded once for each instruction taken out.
55const REMOVED: &str = "instruction with no effects and no users removed";
56
57/// Recorded for an instruction that would have gone if there had been fuel for it.
58const NO_FUEL: &str = "dead instruction kept, the pass ran out of fuel";
59
60/// Recorded once for a function that has an instruction this pass is not allowed to look at.
61///
62/// The honest miss of this pass, and the one worth reading. `has_effects` is conservative, so a
63/// load of a value nothing reads and an allocation nothing addresses both stay, and both of them
64/// are removable once there is a memory analysis to say so. A function with none of these is a
65/// function where this pass found everything there was.
66const NEEDS_MEMORY_ANALYSIS: &str =
67 "instruction with effects left alone, removing it needs a memory analysis";
68
69/// The pass. It holds nothing, because the counts are per function and live in [`Pass::run`].
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct Dce;
72
73impl Pass for Dce {
74 fn name(&self) -> &'static str {
75 "dce"
76 }
77
78 fn describe(&self) -> &'static str {
79 "an instruction with no effects whose results nothing uses is removed"
80 }
81
82 fn preserves(&self) -> Preserved {
83 // Instructions go and blocks do not. A terminator is never dead, because it has an
84 // effect, so no block loses the thing that gives it its edges.
85 Preserved::ALL
86 }
87
88 fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
89 let mut stats = Stats::new();
90 let mut uses = count(func);
91 let mut work: Vec<Inst> = Vec::new();
92 for block in func.blocks().collect::<Vec<Block>>() {
93 for inst in func.insts(block) {
94 match verdict(func, inst, &uses) {
95 Verdict::Dead => work.push(inst),
96 // Nothing reads it and it stays anyway, which is the one thing this pass
97 // gives up on rather than the thousands of instructions that are simply
98 // live. Counted here, in the one walk that sees every instruction, so the
99 // number is per function and not per visit of the worklist.
100 Verdict::Effects => stats.missed(NEEDS_MEMORY_ANALYSIS),
101 Verdict::Used | Verdict::Terminator => {}
102 }
103 }
104 }
105 while let Some(inst) = work.pop() {
106 // A worklist can name the same instruction twice, once from the first walk and once
107 // from an operand reaching zero, and the second visit finds it already gone.
108 if func.block_of(inst).is_none() {
109 continue;
110 }
111 if verdict(func, inst, &uses) != Verdict::Dead {
112 continue;
113 }
114 if !fuel.take() {
115 // Out of fuel, which stops the transforming and not the looking, the same way
116 // folding treats it. Draining the rest of the list without removing anything
117 // costs one pass over what is left and keeps the walk's shape independent of
118 // where the fuel ran out.
119 stats.missed(NO_FUEL);
120 continue;
121 }
122 operands(func, inst, |value| {
123 let count = &mut uses[value.index()];
124 *count -= 1;
125 if *count == 0 {
126 if let Def::Result { inst: def, .. } = func[value].def {
127 work.push(def);
128 }
129 }
130 });
131 func.remove_inst(inst);
132 stats.optimized(REMOVED);
133 }
134 stats
135 }
136}
137
138/// Whether this instruction can go, and when it cannot, what kept it.
139///
140/// The reason is separated out from the answer because two of the three reasons are ordinary and
141/// one of them is worth reporting. Nearly every instruction in a function is [`Verdict::Used`],
142/// which says nothing. [`Verdict::Effects`] is reached only by an instruction nothing reads, and
143/// there are few of those and every one of them is a thing this pass would take if it knew more.
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145enum Verdict {
146 /// Nothing reads it, nothing depends on it happening, and it can go.
147 Dead,
148 /// Something reads one of its results.
149 Used,
150 /// It ends a block, so the block goes with it or neither does.
151 Terminator,
152 /// Nothing reads it and it happens anyway, as far as this pass can tell.
153 Effects,
154}
155
156/// What to do with this instruction.
157fn verdict(func: &Func, inst: Inst, uses: &[u32]) -> Verdict {
158 let data = &func[inst];
159 // `is_terminator` on the function rather than on the opcode, because `asm goto` branches
160 // and its opcode does not say so. Inline assembly has effects either way, so this is belt
161 // and braces, and it is the cheaper of the two mistakes to make.
162 if func.is_terminator(inst) {
163 return Verdict::Terminator;
164 }
165 if !data.results().all(|value| uses[value.index()] == 0) {
166 return Verdict::Used;
167 }
168 if data.opcode.has_effects() {
169 return Verdict::Effects;
170 }
171 debug_assert!(
172 data.opcode != Opcode::InlineAsm,
173 "inline assembly has effects and cannot reach here"
174 );
175 Verdict::Dead
176}
177
178#[cfg(test)]
179mod tests {
180 use rucc_base::Interner;
181 use rucc_ir::{
182 Block, Builder, Flags, Func, MemInfo, MemOrder, Opcode, Restrict, Signature, Type,
183 };
184
185 use crate::stats::Kind;
186 use crate::{Fuel, Pass, dce::Dce};
187
188 /// A function with one block, ready to have instructions appended to it.
189 fn blank() -> (Interner, Func, Block) {
190 let mut names = Interner::new();
191 let name = names.intern("f");
192 let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(32)]));
193 let block = func.create_block();
194 (names, func, block)
195 }
196
197 /// How many instructions are left in a block.
198 fn left(func: &Func, block: Block) -> usize {
199 func.insts(block).count()
200 }
201
202 #[test]
203 fn arithmetic_nothing_reads_goes_away() {
204 let (_, mut func, block) = blank();
205 let mut build = Builder::new(&mut func, block);
206 let a = build.iconst(Type::int(32), 2);
207 let b = build.iconst(Type::int(32), 3);
208 build.binary(Opcode::Add, a, b, Flags::NONE);
209 build.ret(&[a]);
210 assert!(
211 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
212 .changed()
213 );
214 // The add, and then the constant that only it read. A single walk in this order would
215 // have removed the add and left the three behind, which is what the worklist is for.
216 assert_eq!(left(&func, block), 2);
217 }
218
219 #[test]
220 fn arithmetic_something_reads_stays() {
221 let (_, mut func, block) = blank();
222 let mut build = Builder::new(&mut func, block);
223 let a = build.iconst(Type::int(32), 2);
224 let b = build.iconst(Type::int(32), 3);
225 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
226 build.ret(&[sum]);
227 assert!(
228 !Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
229 .changed()
230 );
231 assert_eq!(left(&func, block), 4);
232 }
233
234 #[test]
235 fn a_value_used_twice_is_not_dead_when_one_use_goes() {
236 let (_, mut func, block) = blank();
237 let mut build = Builder::new(&mut func, block);
238 let x = build.iconst(Type::int(32), 7);
239 let kept = build.binary(Opcode::Add, x, x, Flags::NONE);
240 build.binary(Opcode::Add, x, x, Flags::NONE);
241 build.ret(&[kept]);
242 assert!(
243 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
244 .changed()
245 );
246 // Only the second add. Counting a use per instruction rather than per position would
247 // have driven the constant to zero and taken it out from under the first one.
248 assert_eq!(left(&func, block), 3);
249 }
250
251 #[test]
252 fn a_store_stays_however_dead_it_looks() {
253 let (_, mut func, block) = blank();
254 let mut build = Builder::new(&mut func, block);
255 let value = build.iconst(Type::int(32), 1);
256 let address = build.iconst(Type::int(64), 0);
257 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
258 let info = MemInfo {
259 size: 4,
260 align: 4,
261 order: MemOrder::NotAtomic,
262 tbaa: None,
263 restrict: Restrict::NONE,
264 };
265 build.store(value, address, info, Flags::NONE);
266 build.ret(&[value]);
267 let stats =
268 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
269 assert!(!stats.changed());
270 assert_eq!(left(&func, block), 5);
271 // The store is the one instruction here that nothing reads and that stays anyway, so it
272 // is the one this pass reports as a miss. That count is the honest size of what a memory
273 // analysis would buy, per function, without anybody having to guess at it.
274 assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
275 }
276
277 #[test]
278 fn a_value_a_branch_passes_on_is_used_by_the_branch() {
279 let (_, mut func, block) = blank();
280 let target = func.create_block();
281 let param = func.append_param(target, Type::int(32));
282 let mut build = Builder::new(&mut func, block);
283 let x = build.iconst(Type::int(32), 9);
284 build.jump(target, &[x]);
285 let mut build = Builder::new(&mut func, target);
286 build.ret(&[param]);
287 assert!(
288 !Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
289 .changed()
290 );
291 // The constant is read by nothing in its own block and is not dead, because the only
292 // use an instruction can have that its argument list does not hold is this one.
293 assert_eq!(left(&func, block), 2);
294 }
295
296 #[test]
297 fn a_result_a_removed_instruction_read_is_looked_at_again() {
298 let (_, mut func, block) = blank();
299 let mut build = Builder::new(&mut func, block);
300 let a = build.iconst(Type::int(32), 2);
301 let b = build.iconst(Type::int(32), 3);
302 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
303 let doubled = build.binary(Opcode::Add, sum, sum, Flags::NONE);
304 build.unary(Opcode::SExt, doubled, Type::int(64));
305 let kept = build.iconst(Type::int(32), 1);
306 build.ret(&[kept]);
307 assert!(
308 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
309 .changed()
310 );
311 // A chain five long, dead from the far end, and all of it goes in one run. This is the
312 // case a walk in program order finds one instruction of per run.
313 assert_eq!(left(&func, block), 2);
314 }
315
316 #[test]
317 fn fuel_stops_the_removing_and_not_the_looking() {
318 let (_, mut func, block) = blank();
319 let mut build = Builder::new(&mut func, block);
320 let a = build.iconst(Type::int(32), 2);
321 let b = build.iconst(Type::int(32), 3);
322 build.binary(Opcode::Add, a, b, Flags::NONE);
323 build.ret(&[a]);
324 let mut fuel = Fuel::of(1);
325 let stats = Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut fuel);
326 assert!(stats.changed());
327 // The add and nothing after it, so the constant the add was keeping alive stays. One
328 // unit of fuel is one transformation, which is what makes a bisection over it land on
329 // a single site.
330 assert_eq!(left(&func, block), 3);
331 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
332 assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
333 }
334}