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, Value};
50
51use crate::{Fuel, Pass};
52
53/// The pass. It holds nothing, because the counts are per function and live in [`Pass::run`].
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct Dce;
56
57impl Pass for Dce {
58 fn name(&self) -> &'static str {
59 "dce"
60 }
61
62 fn describe(&self) -> &'static str {
63 "an instruction with no effects whose results nothing uses is removed"
64 }
65
66 fn run(&self, func: &mut Func, fuel: &mut Fuel) -> bool {
67 let mut uses = count(func);
68 let mut work: Vec<Inst> = Vec::new();
69 for block in func.blocks().collect::<Vec<Block>>() {
70 for inst in func.insts(block) {
71 if dead(func, inst, &uses) {
72 work.push(inst);
73 }
74 }
75 }
76 let mut changed = false;
77 while let Some(inst) = work.pop() {
78 // A worklist can name the same instruction twice, once from the first walk and once
79 // from an operand reaching zero, and the second visit finds it already gone.
80 if func.block_of(inst).is_none() {
81 continue;
82 }
83 if !dead(func, inst, &uses) {
84 continue;
85 }
86 if !fuel.take() {
87 // Out of fuel, which stops the transforming and not the looking, the same way
88 // folding treats it. Draining the rest of the list without removing anything
89 // costs one pass over what is left and keeps the walk's shape independent of
90 // where the fuel ran out.
91 continue;
92 }
93 operands(func, inst, |value| {
94 let count = &mut uses[value.index()];
95 *count -= 1;
96 if *count == 0 {
97 if let Def::Result { inst: def, .. } = func[value].def {
98 work.push(def);
99 }
100 }
101 });
102 func.remove_inst(inst);
103 changed = true;
104 }
105 changed
106 }
107}
108
109/// How many times each value is used, by position rather than by instruction.
110fn count(func: &Func) -> Vec<u32> {
111 let mut uses = vec![0u32; func.counts().values];
112 for block in func.blocks().collect::<Vec<Block>>() {
113 for inst in func.insts(block).collect::<Vec<Inst>>() {
114 operands(func, inst, |value| uses[value.index()] += 1);
115 }
116 }
117 uses
118}
119
120/// Every value this instruction reads, with a repeat for each time it reads it.
121///
122/// The arguments and the arguments of the blocks it branches to. That is the whole of what an
123/// instruction can use, and it is the same pair the verifier walks, so a use this misses is a use
124/// the verifier would already be looking at from the other side.
125fn operands(func: &Func, inst: Inst, mut each: impl FnMut(Value)) {
126 for &value in &func[func[inst].args] {
127 each(value);
128 }
129 for call in func.successors(inst) {
130 for &value in &func[call.args] {
131 each(value);
132 }
133 }
134}
135
136/// Whether this instruction can go.
137fn dead(func: &Func, inst: Inst, uses: &[u32]) -> bool {
138 let data = &func[inst];
139 // `is_terminator` on the function rather than on the opcode, because `asm goto` branches
140 // and its opcode does not say so. Inline assembly has effects either way, so this is belt
141 // and braces, and it is the cheaper of the two mistakes to make.
142 if func.is_terminator(inst) || data.opcode.has_effects() {
143 return false;
144 }
145 debug_assert!(
146 data.opcode != Opcode::InlineAsm,
147 "inline assembly has effects and cannot reach here"
148 );
149 data.results().all(|value| uses[value.index()] == 0)
150}
151
152#[cfg(test)]
153mod tests {
154 use rucc_base::Interner;
155 use rucc_ir::{Block, Builder, Flags, Func, MemInfo, MemOrder, Opcode, Signature, Type};
156
157 use crate::{Fuel, Pass, dce::Dce};
158
159 /// A function with one block, ready to have instructions appended to it.
160 fn blank() -> (Interner, Func, Block) {
161 let mut names = Interner::new();
162 let name = names.intern("f");
163 let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(32)]));
164 let block = func.create_block();
165 (names, func, block)
166 }
167
168 /// How many instructions are left in a block.
169 fn left(func: &Func, block: Block) -> usize {
170 func.insts(block).count()
171 }
172
173 #[test]
174 fn arithmetic_nothing_reads_goes_away() {
175 let (_, mut func, block) = blank();
176 let mut build = Builder::new(&mut func, block);
177 let a = build.iconst(Type::int(32), 2);
178 let b = build.iconst(Type::int(32), 3);
179 build.binary(Opcode::Add, a, b, Flags::NONE);
180 build.ret(&[a]);
181 assert!(Dce.run(&mut func, &mut Fuel::unlimited()));
182 // The add, and then the constant that only it read. A single walk in this order would
183 // have removed the add and left the three behind, which is what the worklist is for.
184 assert_eq!(left(&func, block), 2);
185 }
186
187 #[test]
188 fn arithmetic_something_reads_stays() {
189 let (_, mut func, block) = blank();
190 let mut build = Builder::new(&mut func, block);
191 let a = build.iconst(Type::int(32), 2);
192 let b = build.iconst(Type::int(32), 3);
193 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
194 build.ret(&[sum]);
195 assert!(!Dce.run(&mut func, &mut Fuel::unlimited()));
196 assert_eq!(left(&func, block), 4);
197 }
198
199 #[test]
200 fn a_value_used_twice_is_not_dead_when_one_use_goes() {
201 let (_, mut func, block) = blank();
202 let mut build = Builder::new(&mut func, block);
203 let x = build.iconst(Type::int(32), 7);
204 let kept = build.binary(Opcode::Add, x, x, Flags::NONE);
205 build.binary(Opcode::Add, x, x, Flags::NONE);
206 build.ret(&[kept]);
207 assert!(Dce.run(&mut func, &mut Fuel::unlimited()));
208 // Only the second add. Counting a use per instruction rather than per position would
209 // have driven the constant to zero and taken it out from under the first one.
210 assert_eq!(left(&func, block), 3);
211 }
212
213 #[test]
214 fn a_store_stays_however_dead_it_looks() {
215 let (_, mut func, block) = blank();
216 let mut build = Builder::new(&mut func, block);
217 let value = build.iconst(Type::int(32), 1);
218 let address = build.iconst(Type::int(64), 0);
219 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
220 let info = MemInfo { size: 4, align: 4, order: MemOrder::NotAtomic, tbaa: None };
221 build.store(value, address, info, Flags::NONE);
222 build.ret(&[value]);
223 assert!(!Dce.run(&mut func, &mut Fuel::unlimited()));
224 assert_eq!(left(&func, block), 5);
225 }
226
227 #[test]
228 fn a_value_a_branch_passes_on_is_used_by_the_branch() {
229 let (_, mut func, block) = blank();
230 let target = func.create_block();
231 let param = func.append_param(target, Type::int(32));
232 let mut build = Builder::new(&mut func, block);
233 let x = build.iconst(Type::int(32), 9);
234 build.jump(target, &[x]);
235 let mut build = Builder::new(&mut func, target);
236 build.ret(&[param]);
237 assert!(!Dce.run(&mut func, &mut Fuel::unlimited()));
238 // The constant is read by nothing in its own block and is not dead, because the only
239 // use an instruction can have that its argument list does not hold is this one.
240 assert_eq!(left(&func, block), 2);
241 }
242
243 #[test]
244 fn a_result_a_removed_instruction_read_is_looked_at_again() {
245 let (_, mut func, block) = blank();
246 let mut build = Builder::new(&mut func, block);
247 let a = build.iconst(Type::int(32), 2);
248 let b = build.iconst(Type::int(32), 3);
249 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
250 let doubled = build.binary(Opcode::Add, sum, sum, Flags::NONE);
251 build.unary(Opcode::SExt, doubled, Type::int(64));
252 let kept = build.iconst(Type::int(32), 1);
253 build.ret(&[kept]);
254 assert!(Dce.run(&mut func, &mut Fuel::unlimited()));
255 // A chain five long, dead from the far end, and all of it goes in one run. This is the
256 // case a walk in program order finds one instruction of per run.
257 assert_eq!(left(&func, block), 2);
258 }
259
260 #[test]
261 fn fuel_stops_the_removing_and_not_the_looking() {
262 let (_, mut func, block) = blank();
263 let mut build = Builder::new(&mut func, block);
264 let a = build.iconst(Type::int(32), 2);
265 let b = build.iconst(Type::int(32), 3);
266 build.binary(Opcode::Add, a, b, Flags::NONE);
267 build.ret(&[a]);
268 let mut fuel = Fuel::of(1);
269 assert!(Dce.run(&mut func, &mut fuel));
270 // The add and nothing after it, so the constant the add was keeping alive stays. One
271 // unit of fuel is one transformation, which is what makes a bisection over it land on
272 // a single site.
273 assert_eq!(left(&func, block), 3);
274 }
275}