Skip to content

Commit ef4c86e

Browse files
committed
Add a utility function for performing a basic form of dead store elimintation on single bbs.
1 parent 30b3f35 commit ef4c86e

File tree

3 files changed

+210
-0
lines changed

3 files changed

+210
-0
lines changed

compiler/rustc_middle/src/mir/mod.rs

+37
Original file line numberDiff line numberDiff line change
@@ -1902,6 +1902,43 @@ impl<'tcx> Place<'tcx> {
19021902
(base, proj)
19031903
})
19041904
}
1905+
1906+
/// Checks if the places may overlap.
1907+
///
1908+
/// The analysis is conservative. Returning `false` indicates that the places definitely do not
1909+
/// overlap; returning `true` does not mean they necessarily do.
1910+
#[inline]
1911+
pub fn may_overlap(self, other: Place<'tcx>) -> bool {
1912+
if self.is_indirect() || other.is_indirect() {
1913+
return true;
1914+
}
1915+
1916+
if self.local != other.local {
1917+
return false;
1918+
}
1919+
1920+
for (p1, p2) in self.projection.iter().zip(other.projection.iter()) {
1921+
match (p1, p2) {
1922+
(ProjectionElem::Field(f1, _), ProjectionElem::Field(f2, _)) if f1 != f2 => {
1923+
return false;
1924+
}
1925+
(
1926+
ProjectionElem::ConstantIndex { offset: o1, from_end: e1, .. },
1927+
ProjectionElem::ConstantIndex { offset: o2, from_end: e2, .. },
1928+
) => {
1929+
if e1 == e2 && o1 != o2 {
1930+
return false;
1931+
}
1932+
}
1933+
// There are other conditions we could check for here with slicing and such, but
1934+
// those checks are error-prone and its unclear if that would really get us much
1935+
(a, b) if a != b => return true,
1936+
(_, _) => (),
1937+
}
1938+
}
1939+
1940+
return true;
1941+
}
19051942
}
19061943

19071944
impl From<Local> for Place<'_> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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+
}

compiler/rustc_mir_transform/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ mod const_debuginfo;
4949
mod const_goto;
5050
mod const_prop;
5151
mod coverage;
52+
mod dead_store_elimination;
5253
mod deaggregator;
5354
mod deduplicate_blocks;
5455
mod dest_prop;

0 commit comments

Comments
 (0)