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::{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}