|
| 1 | +use rustc_middle::{ |
| 2 | + mir::*, |
| 3 | + ty::{List, TyCtxt}, |
| 4 | +}; |
| 5 | +use smallvec::SmallVec; |
| 6 | + |
| 7 | +/// This performs a very basic form of dead store elimintation on the basic block. |
| 8 | +/// |
| 9 | +/// Essentially, we loop over the statements in reverse order. As we go, we maintain a list of |
| 10 | +/// places that will be written to before they are used again. If we find a write to any such place, |
| 11 | +/// we can replace it with a nop. Full details and arguments for correctness are in inline comments. |
| 12 | +#[instrument(level = "debug", skip(tcx))] |
| 13 | +pub fn simple_local_dse<'tcx>(bb: &mut BasicBlockData<'tcx>, tcx: TyCtxt<'tcx>) { |
| 14 | + let mut overwritten: Vec<Place<'tcx>> = Vec::new(); |
| 15 | + // We iterate backwards over the statements in the basic block |
| 16 | + for stmt in bb.statements.iter_mut().rev() { |
| 17 | + // For each statement, compute where it reads from and writes to |
| 18 | + let data = compute_statement_data(stmt, tcx); |
| 19 | + // If the statement definitely derefs something, then assume any write that is visible now |
| 20 | + // is not dead |
| 21 | + let Some(data) = data else { |
| 22 | + overwritten.clear(); |
| 23 | + continue; |
| 24 | + }; |
| 25 | + if let Some(p) = data.stored { |
| 26 | + // If the statement writes somewhere, and we know that a "parent" place is over-written |
| 27 | + // later, the statement can be optimized out. This uses the assumptions that 1) `p` does |
| 28 | + // not include any `Deref`s (this is enforced in `compute_statement_data`) and that 2) |
| 29 | + // this write is the entirety of the statements behavior, ie that knowing that the write |
| 30 | + // is dead lets us remove the statement entirely. |
| 31 | + |
| 32 | + // It may be possible to make this smarter. For example, if a type with no padding has |
| 33 | + // all of its fields overwritten, then the whole type can be considered overwritten. |
| 34 | + // Leave that for the future though. |
| 35 | + if overwritten |
| 36 | + .iter() |
| 37 | + .copied() |
| 38 | + .any(|dp| dp.local == p.local && p.projection[..].starts_with(&dp.projection[..])) |
| 39 | + { |
| 40 | + debug!("Optimizing {:?}", stmt); |
| 41 | + stmt.make_nop(); |
| 42 | + continue; |
| 43 | + } |
| 44 | + |
| 45 | + // If we get here, this write can't be optimized out. We may now be able to add it to |
| 46 | + // `overwritten`, but to do that, we need to check that the place does not contain any |
| 47 | + // non-constant indexing. The values of such indexes may change, which may make the part |
| 48 | + // of memory that the place points to inconsistent. |
| 49 | + if p.projection.iter().all(|x| !matches!(x, ProjectionElem::Index(_))) { |
| 50 | + overwritten.push(p); |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + // We need to kick elements out of `overwritten` if their value was used. |
| 55 | + overwritten.retain(|p1| data.loaded.iter().copied().all(|p2| !p1.may_overlap(p2))) |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +struct StatementData<'tcx> { |
| 60 | + /// The place that this statement writes to, or `None`, if it doesn't write anywhere. If this is |
| 61 | + /// `Some`, it is assumed that the corresponding write represents the |
| 62 | + /// entirety of the statement's behavior. |
| 63 | + stored: Option<Place<'tcx>>, |
| 64 | + /// The places that this statement loads from. If `stored` is `Some(_)`, then it is assumed that |
| 65 | + /// these loads are conditioned on the above store not being optimized out. |
| 66 | + loaded: SmallVec<[Place<'tcx>; 8]>, |
| 67 | +} |
| 68 | + |
| 69 | +/// Returns information about how one statement interacts with memory. |
| 70 | +/// |
| 71 | +/// Returning `None` indicates that execution of this statement accesses memory not inside a local. |
| 72 | +fn compute_statement_data<'tcx>( |
| 73 | + stmt: &Statement<'tcx>, |
| 74 | + tcx: TyCtxt<'tcx>, |
| 75 | +) -> Option<StatementData<'tcx>> { |
| 76 | + let mut loaded = SmallVec::new(); |
| 77 | + |
| 78 | + // Adds a `load` to the `loaded` list. This also adds in any locals that are used as indexes. |
| 79 | + let mut add_load = |p: Place<'tcx>| { |
| 80 | + loaded.push(p); |
| 81 | + for elem in p.projection.iter() { |
| 82 | + if let ProjectionElem::Index(i) = elem { |
| 83 | + loaded.push(Place { local: i, projection: List::empty() }); |
| 84 | + } |
| 85 | + } |
| 86 | + }; |
| 87 | + // Adds the address of `p` to the loaded list. |
| 88 | + let add_address_of = |p: Place<'tcx>, loaded: &mut SmallVec<_>| { |
| 89 | + // First of all, computing the address unconditionally uses any `Index`s that appear in the |
| 90 | + // place. |
| 91 | + for elem in p.projection.iter() { |
| 92 | + if let ProjectionElem::Index(i) = elem { |
| 93 | + loaded.push(Place { local: i, projection: List::empty() }); |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + // Now, we additionally use the place that is outside the innermost `Deref`, since that |
| 98 | + // contains the pointer from which we're computing the address. |
| 99 | + if let Some(i) = p.projection.iter().rposition(|x| x == ProjectionElem::Deref) { |
| 100 | + let prefix = &p.projection[0..i]; |
| 101 | + loaded.push(Place { local: p.local, projection: tcx.intern_place_elems(prefix) }); |
| 102 | + }; |
| 103 | + }; |
| 104 | + |
| 105 | + let mut stored = match &stmt.kind { |
| 106 | + StatementKind::FakeRead(x) => { |
| 107 | + add_load(x.1); |
| 108 | + Some(x.1) |
| 109 | + } |
| 110 | + StatementKind::SetDiscriminant { .. } => { |
| 111 | + // There isn't really a clear place associated with a discriminant, so we won't report one |
| 112 | + None |
| 113 | + } |
| 114 | + StatementKind::StorageDead(x) => Some(Place { local: *x, projection: List::empty() }), |
| 115 | + StatementKind::Retag(_, x) => { |
| 116 | + add_load(**x); |
| 117 | + Some(**x) |
| 118 | + } |
| 119 | + StatementKind::AscribeUserType(x, _) => { |
| 120 | + add_load((**x).0); |
| 121 | + Some((**x).0) |
| 122 | + } |
| 123 | + StatementKind::Coverage(_) | StatementKind::StorageLive(_) | StatementKind::Nop => None, |
| 124 | + StatementKind::CopyNonOverlapping(_) => { |
| 125 | + return None; |
| 126 | + } |
| 127 | + StatementKind::Assign(x) => { |
| 128 | + let mut dest = Some(x.0); |
| 129 | + let src = &x.1; |
| 130 | + match src { |
| 131 | + Rvalue::Use(op) |
| 132 | + | Rvalue::Repeat(op, _) |
| 133 | + | Rvalue::Cast(_, op, _) |
| 134 | + | Rvalue::UnaryOp(_, op) |
| 135 | + | Rvalue::ShallowInitBox(op, _) => { |
| 136 | + op.place().map(&mut add_load); |
| 137 | + } |
| 138 | + Rvalue::Len(p) | Rvalue::Discriminant(p) => { |
| 139 | + add_load(*p); |
| 140 | + } |
| 141 | + Rvalue::Ref(_, _, p) | Rvalue::AddressOf(_, p) => { |
| 142 | + add_address_of(*p, &mut loaded); |
| 143 | + } |
| 144 | + Rvalue::BinaryOp(_, x) | Rvalue::CheckedBinaryOp(_, x) => { |
| 145 | + x.0.place().map(&mut add_load); |
| 146 | + x.1.place().map(&mut add_load); |
| 147 | + } |
| 148 | + Rvalue::Aggregate(_, v) => { |
| 149 | + for op in v { |
| 150 | + op.place().map(&mut add_load); |
| 151 | + } |
| 152 | + } |
| 153 | + Rvalue::ThreadLocalRef(_) => { |
| 154 | + // Creating a thread local ref has side-effects, so don't optimize that away |
| 155 | + dest = None; |
| 156 | + } |
| 157 | + Rvalue::NullaryOp(..) => {} |
| 158 | + }; |
| 159 | + dest |
| 160 | + } |
| 161 | + }; |
| 162 | + if let Some(p) = stored { |
| 163 | + add_address_of(p, &mut loaded); |
| 164 | + |
| 165 | + if p.projection.iter().any(|x| x == ProjectionElem::Deref) { |
| 166 | + // We don't reason about memory, so we cannot optimize this statement |
| 167 | + stored = None; |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + Some(StatementData { stored, loaded }) |
| 172 | +} |
0 commit comments