Skip to main content

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}