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 every value it produces is used by
19//! nothing, and when it does not happen for a reason of its own. [`Opcode::has_effects`] is the
20//! predicate for the last of those and it answers one question for two different things: it means
21//! both that an instruction writes memory or does something the program can observe, and that it
22//! reads memory. An allocation, a call and a `va_arg` are the first and stay. A plain load is only
23//! the second, and it goes.
24//!
25//! Removing a dead load needs no memory analysis, which is why it does not wait for one. It cannot
26//! change what any byte holds, it cannot change what another load sees, and nothing after it can
27//! tell that it did not happen. The only thing it changes is whether the program faults on an
28//! address it was never going to use the bytes of, and that is what a compiler is for. What does
29//! stay is a load the program asked to happen, which is a `volatile` one, and a load other threads
30//! can see the order of, which is an atomic one at any strength.
31//!
32//! An allocation nothing addresses is still removable and still here, and that one does want a
33//! memory analysis, because whether anything addresses it is the question.
34//!
35//! # Why it is a worklist
36//!
37//! Removing an instruction can kill the one that fed it, and that one can kill its own operand, so
38//! a single walk in any order finds a fraction of what is there. The counts are built once, and
39//! removing an instruction decrements what its operands were used for, and an operand that reaches
40//! zero puts its own definition back on the list. That reaches the same fixpoint a repeated walk
41//! would and touches each instruction about once.
42//!
43//! Uses are counted per occurrence rather than per instruction, because `x + x` uses `x` twice and
44//! removing one adder should not make `x` look dead.
45//!
46//! # What it does not remove
47//!
48//! Not a block parameter. A parameter nothing reads is dead in exactly the same sense, and taking
49//! it out means rewriting the argument list of every branch that arrives at the block, which is
50//! worth doing and is a different transformation from this one. The loop carried case is the
51//! interesting one there and it is the reason to do it separately: a parameter whose only use is
52//! the argument it passes to itself is dead, and seeing that needs the cycle broken rather than a
53//! count driven to zero.
54//!
55//! Not an unreachable block. A block no branch names is dead code by any definition, and removing
56//! it is control flow work rather than value work. It belongs with the branch folding that creates
57//! most of it.
58
59use rucc_ir::{Block, Def, Extra, Flags, Func, Inst, MemOrder, Opcode};
60
61use crate::uses::{count, operands};
62use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
63
64/// Recorded once for each instruction taken out.
65const REMOVED: &str = "instruction with no effects and no users removed";
66
67/// Recorded for an instruction that would have gone if there had been fuel for it.
68const NO_FUEL: &str = "dead instruction kept, the pass ran out of fuel";
69
70/// Recorded once for a function that has an instruction this pass is not allowed to look at.
71///
72/// The honest miss of this pass, and the one worth reading. A store nothing can read again, an
73/// allocation nothing addresses and a call that returns nothing and does nothing are all removable
74/// once there is a memory analysis to say so, and all of them stay. A function with none of these
75/// is a function where this pass found everything there was.
76const NEEDS_MEMORY_ANALYSIS: &str =
77    "instruction with effects left alone, removing it needs a memory analysis";
78
79/// The pass. It holds nothing, because the counts are per function and live in [`Pass::run`].
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct Dce;
82
83impl Pass for Dce {
84    fn name(&self) -> &'static str {
85        "dce"
86    }
87
88    fn describe(&self) -> &'static str {
89        "an instruction with no effects whose results nothing uses is removed"
90    }
91
92    fn preserves(&self) -> Preserved {
93        // Instructions go and blocks do not. A terminator is never dead, because it has an
94        // effect, so no block loses the thing that gives it its edges. What does go is a use, and
95        // the last use of a value is the end of its live range, so the liveness is not what it
96        // was and neither is anything counted off it. Nothing had caught this because no pass
97        // before this one in any pipeline builds the liveness, and an analysis nobody has built
98        // is an analysis nobody can be wrong about.
99        Preserved::ALL.without(Analysis::Liveness)
100    }
101
102    fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
103        let mut stats = Stats::new();
104        let mut uses = count(func);
105        let mut work: Vec<Inst> = Vec::new();
106        for block in func.blocks().collect::<Vec<Block>>() {
107            for inst in func.insts(block) {
108                match verdict(func, inst, &uses) {
109                    Verdict::Dead => work.push(inst),
110                    // Nothing reads it and it stays anyway, which is the one thing this pass
111                    // gives up on rather than the thousands of instructions that are simply
112                    // live. Counted here, in the one walk that sees every instruction, so the
113                    // number is per function and not per visit of the worklist.
114                    Verdict::Effects => stats.missed(NEEDS_MEMORY_ANALYSIS),
115                    Verdict::Used | Verdict::Terminator => {}
116                }
117            }
118        }
119        while let Some(inst) = work.pop() {
120            // A worklist can name the same instruction twice, once from the first walk and once
121            // from an operand reaching zero, and the second visit finds it already gone.
122            if func.block_of(inst).is_none() {
123                continue;
124            }
125            if verdict(func, inst, &uses) != Verdict::Dead {
126                continue;
127            }
128            if !fuel.take() {
129                // Out of fuel, which stops the transforming and not the looking, the same way
130                // folding treats it. Draining the rest of the list without removing anything
131                // costs one pass over what is left and keeps the walk's shape independent of
132                // where the fuel ran out.
133                stats.missed(NO_FUEL);
134                continue;
135            }
136            operands(func, inst, |value| {
137                let count = &mut uses[value.index()];
138                *count -= 1;
139                if *count == 0 {
140                    if let Def::Result { inst: def, .. } = func[value].def {
141                        work.push(def);
142                    }
143                }
144            });
145            func.remove_inst(inst);
146            stats.optimized(REMOVED);
147        }
148        stats
149    }
150}
151
152/// Whether this instruction can go, and when it cannot, what kept it.
153///
154/// The reason is separated out from the answer because two of the three reasons are ordinary and
155/// one of them is worth reporting. Nearly every instruction in a function is [`Verdict::Used`],
156/// which says nothing. [`Verdict::Effects`] is reached only by an instruction nothing reads, and
157/// there are few of those and every one of them is a thing this pass would take if it knew more.
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159enum Verdict {
160    /// Nothing reads it, nothing depends on it happening, and it can go.
161    Dead,
162    /// Something reads one of its results.
163    Used,
164    /// It ends a block, so the block goes with it or neither does.
165    Terminator,
166    /// Nothing reads it and it happens anyway, as far as this pass can tell.
167    Effects,
168}
169
170/// Whether this instruction only reads memory, so that not doing it is something nothing can tell.
171///
172/// A plain load and nothing else. A `volatile` load is an access the program asked for by name and
173/// happens whether or not anybody wanted the value. An atomic load is part of an order other
174/// threads can see, at every strength and not only at the fence-like ones, and there is no reason
175/// to argue about the weak end of that until something is waiting on the answer.
176fn reads_only(func: &Func, inst: Inst) -> bool {
177    let data = &func[inst];
178    if data.opcode != Opcode::Load || data.flags.contains(Flags::VOLATILE) {
179        return false;
180    }
181    let Extra::Mem(mem) = data.extra else { return false };
182    func[mem].order == MemOrder::NotAtomic
183}
184
185/// What to do with this instruction.
186fn verdict(func: &Func, inst: Inst, uses: &[u32]) -> Verdict {
187    let data = &func[inst];
188    // `is_terminator` on the function rather than on the opcode, because `asm goto` branches
189    // and its opcode does not say so. Inline assembly has effects either way, so this is belt
190    // and braces, and it is the cheaper of the two mistakes to make.
191    if func.is_terminator(inst) {
192        return Verdict::Terminator;
193    }
194    if !data.results().all(|value| uses[value.index()] == 0) {
195        return Verdict::Used;
196    }
197    if data.opcode.has_effects() && !reads_only(func, inst) {
198        return Verdict::Effects;
199    }
200    debug_assert!(
201        data.opcode != Opcode::InlineAsm,
202        "inline assembly has effects and cannot reach here"
203    );
204    Verdict::Dead
205}
206
207#[cfg(test)]
208mod tests {
209    use rucc_base::Interner;
210    use rucc_ir::{
211        Block, Builder, Flags, Func, MemInfo, MemOrder, Opcode, Restrict, Signature, Type,
212    };
213
214    use crate::stats::Kind;
215    use crate::{Analysis, Fuel, Pass, dce::Dce};
216
217    /// A function with one block, ready to have instructions appended to it.
218    fn blank() -> (Interner, Func, Block) {
219        let mut names = Interner::new();
220        let name = names.intern("f");
221        let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(32)]));
222        let block = func.create_block();
223        (names, func, block)
224    }
225
226    /// A four byte access of that strength, with nothing else said about it.
227    fn plain(order: MemOrder) -> MemInfo {
228        MemInfo { size: 4, align: 4, owns: 4, order, tbaa: None, restrict: Restrict::NONE }
229    }
230
231    /// How many instructions are left in a block.
232    fn left(func: &Func, block: Block) -> usize {
233        func.insts(block).count()
234    }
235
236    #[test]
237    fn arithmetic_nothing_reads_goes_away() {
238        let (_, mut func, block) = blank();
239        let mut build = Builder::new(&mut func, block);
240        let a = build.iconst(Type::int(32), 2);
241        let b = build.iconst(Type::int(32), 3);
242        build.binary(Opcode::Add, a, b, Flags::NONE);
243        build.ret(&[a]);
244        assert!(
245            Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
246                .changed()
247        );
248        // The add, and then the constant that only it read. A single walk in this order would
249        // have removed the add and left the three behind, which is what the worklist is for.
250        assert_eq!(left(&func, block), 2);
251    }
252
253    #[test]
254    fn the_counts_in_the_cache_go_with_the_uses_that_were_removed() {
255        let (_, mut func, block) = blank();
256        let mut build = Builder::new(&mut func, block);
257        let a = build.iconst(Type::int(32), 2);
258        let b = build.iconst(Type::int(32), 3);
259        build.binary(Opcode::Add, a, b, Flags::NONE);
260        build.ret(&[a]);
261        let mut an = crate::machine::fixtures::analyses();
262        // Two values are live where the add is and one is live once it has gone, which is the
263        // fact this pass used to say it had left standing.
264        an.pressure(&func);
265        assert!(Dce.run(&mut func, &mut an, &mut Fuel::unlimited()).changed());
266        assert!(an.settle(&func, Dce.preserves(), true).is_empty(), "the pass was caught out");
267        assert!(!an.holds(Analysis::Pressure), "a stale count was left for the next pass to read");
268    }
269
270    #[test]
271    fn arithmetic_something_reads_stays() {
272        let (_, mut func, block) = blank();
273        let mut build = Builder::new(&mut func, block);
274        let a = build.iconst(Type::int(32), 2);
275        let b = build.iconst(Type::int(32), 3);
276        let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
277        build.ret(&[sum]);
278        assert!(
279            !Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
280                .changed()
281        );
282        assert_eq!(left(&func, block), 4);
283    }
284
285    #[test]
286    fn a_value_used_twice_is_not_dead_when_one_use_goes() {
287        let (_, mut func, block) = blank();
288        let mut build = Builder::new(&mut func, block);
289        let x = build.iconst(Type::int(32), 7);
290        let kept = build.binary(Opcode::Add, x, x, Flags::NONE);
291        build.binary(Opcode::Add, x, x, Flags::NONE);
292        build.ret(&[kept]);
293        assert!(
294            Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
295                .changed()
296        );
297        // Only the second add. Counting a use per instruction rather than per position would
298        // have driven the constant to zero and taken it out from under the first one.
299        assert_eq!(left(&func, block), 3);
300    }
301
302    #[test]
303    fn a_store_stays_however_dead_it_looks() {
304        let (_, mut func, block) = blank();
305        let mut build = Builder::new(&mut func, block);
306        let value = build.iconst(Type::int(32), 1);
307        let address = build.iconst(Type::int(64), 0);
308        let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
309        let info = MemInfo {
310            size: 4,
311            align: 4,
312            order: MemOrder::NotAtomic,
313            tbaa: None,
314            owns: 0,
315            restrict: Restrict::NONE,
316        };
317        build.store(value, address, info, Flags::NONE);
318        build.ret(&[value]);
319        let stats =
320            Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
321        assert!(!stats.changed());
322        assert_eq!(left(&func, block), 5);
323        // The store is the one instruction here that nothing reads and that stays anyway, so it
324        // is the one this pass reports as a miss. That count is the honest size of what a memory
325        // analysis would buy, per function, without anybody having to guess at it.
326        assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
327    }
328
329    /// A plain load nothing reads goes, which is the one thing here that does not wait for a
330    /// memory analysis. Removing it cannot change what any byte holds or what another load sees.
331    #[test]
332    fn a_load_nothing_reads_goes_away() {
333        let (_, mut func, block) = blank();
334        let mut build = Builder::new(&mut func, block);
335        let address = build.iconst(Type::int(64), 0);
336        let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
337        build.load(Type::int(32), address, plain(MemOrder::NotAtomic), Flags::NONE);
338        let kept = build.iconst(Type::int(32), 1);
339        build.ret(&[kept]);
340        let stats =
341            Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
342        assert!(stats.changed());
343        // The load, then the cast and the constant that only it read, so what is left is the
344        // constant the return reads and the return.
345        assert_eq!(left(&func, block), 2);
346        assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 0);
347    }
348
349    /// A `volatile` load stays. It is an access the program asked for by name, and it happens
350    /// whether or not anybody wanted the value it produced.
351    #[test]
352    fn a_volatile_load_nothing_reads_stays() {
353        let (_, mut func, block) = blank();
354        let mut build = Builder::new(&mut func, block);
355        let address = build.iconst(Type::int(64), 0);
356        let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
357        build.load(Type::int(32), address, plain(MemOrder::NotAtomic), Flags::VOLATILE);
358        let kept = build.iconst(Type::int(32), 1);
359        build.ret(&[kept]);
360        let stats =
361            Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
362        assert!(!stats.changed());
363        assert_eq!(left(&func, block), 5);
364        assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
365    }
366
367    /// An atomic load stays at every strength, because what it is part of is an order other
368    /// threads can see rather than the value it hands back.
369    #[test]
370    fn an_atomic_load_nothing_reads_stays_however_weak_it_is() {
371        for order in [MemOrder::Relaxed, MemOrder::Acquire, MemOrder::SeqCst] {
372            let (_, mut func, block) = blank();
373            let mut build = Builder::new(&mut func, block);
374            let address = build.iconst(Type::int(64), 0);
375            let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
376            build.load(Type::int(32), address, plain(order), Flags::NONE);
377            let kept = build.iconst(Type::int(32), 1);
378            build.ret(&[kept]);
379            let stats = Dce.run(
380                &mut func,
381                &mut crate::machine::fixtures::analyses(),
382                &mut Fuel::unlimited(),
383            );
384            assert!(!stats.changed(), "{order:?}");
385            assert_eq!(left(&func, block), 5, "{order:?}");
386        }
387    }
388
389    #[test]
390    fn a_value_a_branch_passes_on_is_used_by_the_branch() {
391        let (_, mut func, block) = blank();
392        let target = func.create_block();
393        let param = func.append_param(target, Type::int(32));
394        let mut build = Builder::new(&mut func, block);
395        let x = build.iconst(Type::int(32), 9);
396        build.jump(target, &[x]);
397        let mut build = Builder::new(&mut func, target);
398        build.ret(&[param]);
399        assert!(
400            !Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
401                .changed()
402        );
403        // The constant is read by nothing in its own block and is not dead, because the only
404        // use an instruction can have that its argument list does not hold is this one.
405        assert_eq!(left(&func, block), 2);
406    }
407
408    #[test]
409    fn a_result_a_removed_instruction_read_is_looked_at_again() {
410        let (_, mut func, block) = blank();
411        let mut build = Builder::new(&mut func, block);
412        let a = build.iconst(Type::int(32), 2);
413        let b = build.iconst(Type::int(32), 3);
414        let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
415        let doubled = build.binary(Opcode::Add, sum, sum, Flags::NONE);
416        build.unary(Opcode::SExt, doubled, Type::int(64));
417        let kept = build.iconst(Type::int(32), 1);
418        build.ret(&[kept]);
419        assert!(
420            Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
421                .changed()
422        );
423        // A chain five long, dead from the far end, and all of it goes in one run. This is the
424        // case a walk in program order finds one instruction of per run.
425        assert_eq!(left(&func, block), 2);
426    }
427
428    #[test]
429    fn fuel_stops_the_removing_and_not_the_looking() {
430        let (_, mut func, block) = blank();
431        let mut build = Builder::new(&mut func, block);
432        let a = build.iconst(Type::int(32), 2);
433        let b = build.iconst(Type::int(32), 3);
434        build.binary(Opcode::Add, a, b, Flags::NONE);
435        build.ret(&[a]);
436        let mut fuel = Fuel::of(1);
437        let stats = Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut fuel);
438        assert!(stats.changed());
439        // The add and nothing after it, so the constant the add was keeping alive stays. One
440        // unit of fuel is one transformation, which is what makes a bisection over it land on
441        // a single site.
442        assert_eq!(left(&func, block), 3);
443        assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
444        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
445    }
446}