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::{Analyses, 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!(Dce.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
211 // The add, and then the constant that only it read. A single walk in this order would
212 // have removed the add and left the three behind, which is what the worklist is for.
213 assert_eq!(left(&func, block), 2);
214 }
215
216 #[test]
217 fn arithmetic_something_reads_stays() {
218 let (_, mut func, block) = blank();
219 let mut build = Builder::new(&mut func, block);
220 let a = build.iconst(Type::int(32), 2);
221 let b = build.iconst(Type::int(32), 3);
222 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
223 build.ret(&[sum]);
224 assert!(!Dce.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
225 assert_eq!(left(&func, block), 4);
226 }
227
228 #[test]
229 fn a_value_used_twice_is_not_dead_when_one_use_goes() {
230 let (_, mut func, block) = blank();
231 let mut build = Builder::new(&mut func, block);
232 let x = build.iconst(Type::int(32), 7);
233 let kept = build.binary(Opcode::Add, x, x, Flags::NONE);
234 build.binary(Opcode::Add, x, x, Flags::NONE);
235 build.ret(&[kept]);
236 assert!(Dce.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
237 // Only the second add. Counting a use per instruction rather than per position would
238 // have driven the constant to zero and taken it out from under the first one.
239 assert_eq!(left(&func, block), 3);
240 }
241
242 #[test]
243 fn a_store_stays_however_dead_it_looks() {
244 let (_, mut func, block) = blank();
245 let mut build = Builder::new(&mut func, block);
246 let value = build.iconst(Type::int(32), 1);
247 let address = build.iconst(Type::int(64), 0);
248 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
249 let info = MemInfo {
250 size: 4,
251 align: 4,
252 order: MemOrder::NotAtomic,
253 tbaa: None,
254 restrict: Restrict::NONE,
255 };
256 build.store(value, address, info, Flags::NONE);
257 build.ret(&[value]);
258 let stats = Dce.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited());
259 assert!(!stats.changed());
260 assert_eq!(left(&func, block), 5);
261 // The store is the one instruction here that nothing reads and that stays anyway, so it
262 // is the one this pass reports as a miss. That count is the honest size of what a memory
263 // analysis would buy, per function, without anybody having to guess at it.
264 assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
265 }
266
267 #[test]
268 fn a_value_a_branch_passes_on_is_used_by_the_branch() {
269 let (_, mut func, block) = blank();
270 let target = func.create_block();
271 let param = func.append_param(target, Type::int(32));
272 let mut build = Builder::new(&mut func, block);
273 let x = build.iconst(Type::int(32), 9);
274 build.jump(target, &[x]);
275 let mut build = Builder::new(&mut func, target);
276 build.ret(&[param]);
277 assert!(!Dce.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
278 // The constant is read by nothing in its own block and is not dead, because the only
279 // use an instruction can have that its argument list does not hold is this one.
280 assert_eq!(left(&func, block), 2);
281 }
282
283 #[test]
284 fn a_result_a_removed_instruction_read_is_looked_at_again() {
285 let (_, mut func, block) = blank();
286 let mut build = Builder::new(&mut func, block);
287 let a = build.iconst(Type::int(32), 2);
288 let b = build.iconst(Type::int(32), 3);
289 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
290 let doubled = build.binary(Opcode::Add, sum, sum, Flags::NONE);
291 build.unary(Opcode::SExt, doubled, Type::int(64));
292 let kept = build.iconst(Type::int(32), 1);
293 build.ret(&[kept]);
294 assert!(Dce.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
295 // A chain five long, dead from the far end, and all of it goes in one run. This is the
296 // case a walk in program order finds one instruction of per run.
297 assert_eq!(left(&func, block), 2);
298 }
299
300 #[test]
301 fn fuel_stops_the_removing_and_not_the_looking() {
302 let (_, mut func, block) = blank();
303 let mut build = Builder::new(&mut func, block);
304 let a = build.iconst(Type::int(32), 2);
305 let b = build.iconst(Type::int(32), 3);
306 build.binary(Opcode::Add, a, b, Flags::NONE);
307 build.ret(&[a]);
308 let mut fuel = Fuel::of(1);
309 let stats = Dce.run(&mut func, &mut Analyses::new(), &mut fuel);
310 assert!(stats.changed());
311 // The add and nothing after it, so the constant the add was keeping alive stays. One
312 // unit of fuel is one transformation, which is what makes a bisection over it land on
313 // a single site.
314 assert_eq!(left(&func, block), 3);
315 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
316 assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
317 }
318}