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};
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}