Skip to main content

rustc_mir_transform/
dead_store_elimination.rs

1//! This module implements a dead store elimination (DSE) routine.
2//!
3//! This transformation was written specifically for the needs of dest prop. Although it is
4//! perfectly sound to use it in any context that might need it, its behavior should not be changed
5//! without analyzing the interaction this will have with dest prop. Specifically, in addition to
6//! the soundness of this pass in general, dest prop needs it to satisfy two additional conditions:
7//!
8//!  1. It's idempotent, meaning that running this pass a second time immediately after running it a
9//!     first time will not cause any further changes.
10//!  2. This idempotence persists across dest prop's main transform, in other words inserting any
11//!     number of iterations of dest prop between the first and second application of this transform
12//!     will still not cause any further changes.
13//!
14
15use rustc_middle::mir::visit::Visitor;
16use rustc_middle::mir::*;
17use rustc_middle::ty::TyCtxt;
18use rustc_mir_dataflow::Analysis;
19use rustc_mir_dataflow::debuginfo::debuginfo_locals;
20use rustc_mir_dataflow::impls::{
21    LivenessTransferFunction, MaybeTransitiveLiveLocals, borrowed_locals,
22};
23use rustc_span::bug;
24
25use crate::PassPolicy;
26use crate::simplify::UsedInStmtLocals;
27use crate::util::most_packed_projection;
28
29/// Performs the optimization on the body
30///
31/// The `borrowed` set must be a `DenseBitSet` of all the locals that are ever borrowed in this
32/// body. It can be generated via the [`borrowed_locals`] function.
33/// Returns true if any instruction is eliminated.
34fn eliminate<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> bool {
35    let borrowed_locals = borrowed_locals(body);
36
37    // If the user requests complete debuginfo, mark the locals that appear in it as live, so
38    // we don't remove assignments to them.
39    let debuginfo_locals = debuginfo_locals(body);
40
41    let mut live = MaybeTransitiveLiveLocals::new(&borrowed_locals, &debuginfo_locals)
42        .iterate_to_fixpoint(tcx, body, None)
43        .into_results_cursor(body);
44
45    // For blocks with a call terminator, if an argument copy can be turned into a move,
46    // record it as (block, argument index).
47    let mut call_operands_to_move = Vec::new();
48    let mut patch = Vec::new();
49
50    for (bb, bb_data) in traversal::preorder(body) {
51        if let TerminatorKind::Call { ref args, ref destination, .. } = bb_data.terminator().kind {
52            let loc = Location { block: bb, statement_index: bb_data.statements.len() };
53
54            // Position ourselves between the evaluation of `args` and the write to `destination`.
55            live.seek_to_block_end(bb);
56            let mut state = live.get().clone();
57
58            // Don't turn into a move if the local is used as an index
59            // projection for the destination place.
60            LivenessTransferFunction(&mut state).visit_place(
61                destination,
62                visit::PlaceContext::MutatingUse(visit::MutatingUseContext::Call),
63                loc,
64            );
65
66            // The logic in LivenessTransferFunction isn't quite what we need; it ignores call
67            // destinations that are just locals because they are killed by the call, which makes
68            // it eligible to be moved-from in the argument list. That's backwards.
69            if !destination.is_indirect() {
70                state.insert(destination.local);
71            }
72
73            for (index, arg) in args.iter().map(|a| &a.node).enumerate().rev() {
74                if let Operand::Copy(place) = *arg
75                    && !place.is_indirect()
76                    // Do not skip the transformation if the local is in debuginfo, as we do
77                    // not really lose any information for this purpose.
78                    && !borrowed_locals.contains(place.local)
79                    && !state.contains(place.local)
80                    // If `place` is a projection of a disaligned field in a packed ADT,
81                    // the move may be codegened as a pointer to that field.
82                    // Using that disaligned pointer may trigger UB in the callee,
83                    // so do nothing.
84                    && most_packed_projection(tcx, body, place).is_none()
85                {
86                    call_operands_to_move.push((bb, index));
87                }
88
89                // Account that `arg` is read from, so we don't promote another argument to a move.
90                LivenessTransferFunction(&mut state).visit_operand(arg, loc);
91            }
92        }
93
94        for (statement_index, statement) in bb_data.statements.iter().enumerate().rev() {
95            if let Some(destination) = MaybeTransitiveLiveLocals::can_be_removed_if_dead(
96                &statement.kind,
97                &borrowed_locals,
98                &debuginfo_locals,
99            ) {
100                let loc = Location { block: bb, statement_index };
101                live.seek_before_primary_effect(loc);
102                if !live.get().contains(destination.local) {
103                    let drop_debuginfo = !debuginfo_locals.contains(destination.local);
104                    // When eliminating a dead statement, we need to address
105                    // the debug information for that statement.
106                    assert!(
107                        drop_debuginfo || statement.kind.as_debuginfo().is_some(),
108                        "don't know how to retain the debug information for {:?}",
109                        statement.kind
110                    );
111                    patch.push((loc, drop_debuginfo));
112                }
113            }
114        }
115    }
116
117    if patch.is_empty() && call_operands_to_move.is_empty() {
118        return false;
119    }
120    let eliminated = !patch.is_empty();
121
122    let bbs = body.basic_blocks.as_mut_preserves_cfg();
123    for (Location { block, statement_index }, drop_debuginfo) in patch {
124        bbs[block].statements[statement_index].make_nop(drop_debuginfo);
125    }
126    for (block, argument_index) in call_operands_to_move {
127        let TerminatorKind::Call { ref mut args, .. } = bbs[block].terminator_mut().kind else {
128            bug!()
129        };
130        let arg = &mut args[argument_index].node;
131        let Operand::Copy(place) = *arg else { bug!() };
132        *arg = Operand::Move(place);
133    }
134
135    eliminated
136}
137
138pub(super) enum DeadStoreElimination {
139    Initial,
140    Final,
141}
142
143impl<'tcx> crate::MirPass<'tcx> for DeadStoreElimination {
144    fn name(&self) -> &'static str {
145        match self {
146            DeadStoreElimination::Initial => "DeadStoreElimination-initial",
147            DeadStoreElimination::Final => "DeadStoreElimination-final",
148        }
149    }
150
151    fn policy(&self, ctx: &crate::PassCtx<'_>) -> PassPolicy {
152        PassPolicy::optional(ctx.mir_opt_level() >= 2)
153    }
154
155    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
156        if eliminate(tcx, body) {
157            UsedInStmtLocals::new(body).remove_unused_storage_annotations(body);
158            for data in body.basic_blocks.as_mut_preserves_cfg() {
159                data.strip_nops();
160            }
161        }
162    }
163}