Skip to content

Commit 5632001

Browse files
committed
Improve copy_prop and GVN mir-opt passes to remove fewer storage calls
1 parent 0006519 commit 5632001

213 files changed

Lines changed: 2072 additions & 1062 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

compiler/rustc_mir_transform/src/copy_prop.rs

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ use rustc_index::bit_set::DenseBitSet;
33
use rustc_middle::mir::visit::*;
44
use rustc_middle::mir::*;
55
use rustc_middle::ty::TyCtxt;
6+
use rustc_mir_dataflow::{Analysis, ResultsCursor};
67
use tracing::{debug, instrument};
78

8-
use crate::ssa::SsaLocals;
9+
use crate::ssa::{MaybeUninitializedLocals, SsaLocals};
910

1011
/// Unify locals that copy each other.
1112
///
@@ -16,7 +17,7 @@ use crate::ssa::SsaLocals;
1617
/// _d = move? _c
1718
/// where each of the locals is only assigned once.
1819
///
19-
/// We want to replace all those locals by `copy _a`.
20+
/// We want to replace all those locals by `_a` (the "head"), either copied or moved.
2021
pub(super) struct CopyProp;
2122

2223
impl<'tcx> crate::MirPass<'tcx> for CopyProp {
@@ -30,15 +31,19 @@ impl<'tcx> crate::MirPass<'tcx> for CopyProp {
3031

3132
let typing_env = body.typing_env(tcx);
3233
let ssa = SsaLocals::new(tcx, body, typing_env);
34+
3335
debug!(borrowed_locals = ?ssa.borrowed_locals());
3436
debug!(copy_classes = ?ssa.copy_classes());
3537

3638
let mut any_replacement = false;
3739
// Locals that participate in copy propagation either as a source or a destination.
3840
let mut unified = DenseBitSet::new_empty(body.local_decls.len());
41+
let mut storage_to_remove = DenseBitSet::new_empty(body.local_decls.len());
42+
3943
for (local, &head) in ssa.copy_classes().iter_enumerated() {
4044
if local != head {
4145
any_replacement = true;
46+
storage_to_remove.insert(head);
4247
unified.insert(head);
4348
unified.insert(local);
4449
}
@@ -48,7 +53,46 @@ impl<'tcx> crate::MirPass<'tcx> for CopyProp {
4853
return;
4954
}
5055

51-
Replacer { tcx, copy_classes: ssa.copy_classes(), unified }.visit_body_preserves_cfg(body);
56+
// When emitting storage statements, we want to retain the head locals' storage statements,
57+
// as this enables better optimizations. For each local use location, we mark the head for storage removal
58+
// only if the head might be uninitialized at that point, or if the local is borrowed
59+
// (since we cannot easily determine when it's used).
60+
let storage_to_remove = if tcx.sess.emit_lifetime_markers() {
61+
storage_to_remove.clear();
62+
63+
// If the local is borrowed, we cannot easily determine if it is used, so we have to remove the storage statements.
64+
let borrowed_locals = ssa.borrowed_locals();
65+
66+
for (local, &head) in ssa.copy_classes().iter_enumerated() {
67+
if local != head && borrowed_locals.contains(local) {
68+
storage_to_remove.insert(head);
69+
}
70+
}
71+
72+
let maybe_uninit = MaybeUninitializedLocals
73+
.iterate_to_fixpoint(tcx, body, Some("mir_opt::copy_prop"))
74+
.into_results_cursor(body);
75+
76+
let mut storage_checker = StorageChecker {
77+
maybe_uninit,
78+
copy_classes: ssa.copy_classes(),
79+
storage_to_remove,
80+
};
81+
82+
for (bb, data) in traversal::reachable(body) {
83+
storage_checker.visit_basic_block_data(bb, data);
84+
}
85+
86+
storage_checker.storage_to_remove
87+
} else {
88+
// Remove the storage statements of all the head locals.
89+
storage_to_remove
90+
};
91+
92+
debug!(?storage_to_remove);
93+
94+
Replacer { tcx, copy_classes: ssa.copy_classes(), unified, storage_to_remove }
95+
.visit_body_preserves_cfg(body);
5296

5397
crate::simplify::remove_unused_definitions(body);
5498
}
@@ -63,6 +107,7 @@ impl<'tcx> crate::MirPass<'tcx> for CopyProp {
63107
struct Replacer<'a, 'tcx> {
64108
tcx: TyCtxt<'tcx>,
65109
unified: DenseBitSet<Local>,
110+
storage_to_remove: DenseBitSet<Local>,
66111
copy_classes: &'a IndexSlice<Local, Local>,
67112
}
68113

@@ -73,7 +118,13 @@ impl<'tcx> MutVisitor<'tcx> for Replacer<'_, 'tcx> {
73118

74119
#[tracing::instrument(level = "trace", skip(self))]
75120
fn visit_local(&mut self, local: &mut Local, ctxt: PlaceContext, _: Location) {
76-
*local = self.copy_classes[*local];
121+
let new_local = self.copy_classes[*local];
122+
match ctxt {
123+
// Do not modify the local in storage statements.
124+
PlaceContext::NonUse(NonUseContext::StorageLive | NonUseContext::StorageDead) => {}
125+
// We access the value.
126+
_ => *local = new_local,
127+
}
77128
}
78129

79130
#[tracing::instrument(level = "trace", skip(self))]
@@ -93,7 +144,7 @@ impl<'tcx> MutVisitor<'tcx> for Replacer<'_, 'tcx> {
93144
fn visit_statement(&mut self, stmt: &mut Statement<'tcx>, loc: Location) {
94145
// When removing storage statements, we need to remove both (#107511).
95146
if let StatementKind::StorageLive(l) | StatementKind::StorageDead(l) = stmt.kind
96-
&& self.unified.contains(l)
147+
&& self.storage_to_remove.contains(l)
97148
{
98149
stmt.make_nop(true);
99150
}
@@ -109,3 +160,39 @@ impl<'tcx> MutVisitor<'tcx> for Replacer<'_, 'tcx> {
109160
}
110161
}
111162
}
163+
164+
// Marks heads of copy classes that are maybe uninitialized at the location of a local
165+
// as needing storage statement removal.
166+
struct StorageChecker<'a, 'tcx> {
167+
maybe_uninit: ResultsCursor<'a, 'tcx, MaybeUninitializedLocals>,
168+
copy_classes: &'a IndexSlice<Local, Local>,
169+
storage_to_remove: DenseBitSet<Local>,
170+
}
171+
172+
impl<'a, 'tcx> Visitor<'tcx> for StorageChecker<'a, 'tcx> {
173+
fn visit_local(&mut self, local: Local, context: PlaceContext, loc: Location) {
174+
if !context.is_use() {
175+
return;
176+
}
177+
178+
let head = self.copy_classes[local];
179+
180+
// If the local is the head, or if we already marked it for deletion, we do not need to check it.
181+
if head == local || self.storage_to_remove.contains(head) {
182+
return;
183+
}
184+
185+
self.maybe_uninit.seek_before_primary_effect(loc);
186+
187+
if self.maybe_uninit.get().contains(head) {
188+
debug!(
189+
?loc,
190+
?context,
191+
?local,
192+
?head,
193+
"local's head is maybe uninit at this location, marking head for storage statement removal"
194+
);
195+
self.storage_to_remove.insert(head);
196+
}
197+
}
198+
}

compiler/rustc_mir_transform/src/gvn.rs

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,12 @@ use rustc_middle::mir::visit::*;
117117
use rustc_middle::mir::*;
118118
use rustc_middle::ty::layout::HasTypingEnv;
119119
use rustc_middle::ty::{self, Ty, TyCtxt};
120+
use rustc_mir_dataflow::{Analysis, ResultsCursor};
120121
use rustc_span::DUMMY_SP;
121122
use smallvec::SmallVec;
122123
use tracing::{debug, instrument, trace};
123124

124-
use crate::ssa::SsaLocals;
125+
use crate::ssa::{MaybeUninitializedLocals, SsaLocals};
125126

126127
pub(super) struct GVN;
127128

@@ -154,10 +155,34 @@ impl<'tcx> crate::MirPass<'tcx> for GVN {
154155
state.visit_basic_block_data(bb, data);
155156
}
156157

157-
// For each local that is reused (`y` above), we remove its storage statements do avoid any
158-
// difficulty. Those locals are SSA, so should be easy to optimize by LLVM without storage
159-
// statements.
160-
StorageRemover { tcx, reused_locals: state.reused_locals }.visit_body_preserves_cfg(body);
158+
// When emitting storage statements, we want to retain the reused locals' storage statements,
159+
// as this enables better optimizations. For each local use location, we mark it for storage removal
160+
// only if it might be uninitialized at that point.
161+
let storage_to_remove = if tcx.sess.emit_lifetime_markers() {
162+
let maybe_uninit = MaybeUninitializedLocals
163+
.iterate_to_fixpoint(tcx, body, Some("mir_opt::gvn"))
164+
.into_results_cursor(body);
165+
166+
let mut storage_checker = StorageChecker {
167+
reused_locals: &state.reused_locals,
168+
storage_to_remove: DenseBitSet::new_empty(body.local_decls.len()),
169+
maybe_uninit,
170+
};
171+
172+
for (bb, data) in traversal::reachable(body) {
173+
storage_checker.visit_basic_block_data(bb, data);
174+
}
175+
176+
storage_checker.storage_to_remove
177+
} else {
178+
// Remove the storage statements of all the reused locals.
179+
state.reused_locals.clone()
180+
};
181+
182+
debug!(?storage_to_remove);
183+
184+
StorageRemover { tcx, reused_locals: state.reused_locals, storage_to_remove }
185+
.visit_body_preserves_cfg(body);
161186
}
162187

163188
fn is_required(&self) -> bool {
@@ -2033,6 +2058,7 @@ impl<'tcx> MutVisitor<'tcx> for VnState<'_, '_, 'tcx> {
20332058
struct StorageRemover<'tcx> {
20342059
tcx: TyCtxt<'tcx>,
20352060
reused_locals: DenseBitSet<Local>,
2061+
storage_to_remove: DenseBitSet<Local>,
20362062
}
20372063

20382064
impl<'tcx> MutVisitor<'tcx> for StorageRemover<'tcx> {
@@ -2053,11 +2079,53 @@ impl<'tcx> MutVisitor<'tcx> for StorageRemover<'tcx> {
20532079
match stmt.kind {
20542080
// When removing storage statements, we need to remove both (#107511).
20552081
StatementKind::StorageLive(l) | StatementKind::StorageDead(l)
2056-
if self.reused_locals.contains(l) =>
2082+
if self.storage_to_remove.contains(l) =>
20572083
{
20582084
stmt.make_nop(true)
20592085
}
20602086
_ => self.super_statement(stmt, loc),
20612087
}
20622088
}
20632089
}
2090+
2091+
struct StorageChecker<'a, 'tcx> {
2092+
reused_locals: &'a DenseBitSet<Local>,
2093+
storage_to_remove: DenseBitSet<Local>,
2094+
maybe_uninit: ResultsCursor<'a, 'tcx, MaybeUninitializedLocals>,
2095+
}
2096+
2097+
impl<'a, 'tcx> Visitor<'tcx> for StorageChecker<'a, 'tcx> {
2098+
fn visit_local(&mut self, local: Local, context: PlaceContext, location: Location) {
2099+
match context {
2100+
// These mutating uses do not require the local to be initialized,
2101+
// so we cannot use our maybe-uninit check on them.
2102+
// However, GVN doesn't introduce or move mutations,
2103+
// so this local must already have valid storage at this location.
2104+
PlaceContext::MutatingUse(MutatingUseContext::AsmOutput)
2105+
| PlaceContext::MutatingUse(MutatingUseContext::Call)
2106+
| PlaceContext::MutatingUse(MutatingUseContext::Store)
2107+
| PlaceContext::MutatingUse(MutatingUseContext::Yield)
2108+
| PlaceContext::NonUse(_) => {
2109+
return;
2110+
}
2111+
// Must check validity for other mutating usages and all non-mutating uses.
2112+
PlaceContext::MutatingUse(_) | PlaceContext::NonMutatingUse(_) => {}
2113+
}
2114+
2115+
// We only need to check reused locals which we haven't already removed storage for.
2116+
if !self.reused_locals.contains(local) || self.storage_to_remove.contains(local) {
2117+
return;
2118+
}
2119+
2120+
self.maybe_uninit.seek_before_primary_effect(location);
2121+
2122+
if self.maybe_uninit.get().contains(local) {
2123+
debug!(
2124+
?location,
2125+
?local,
2126+
"local is reused and is maybe uninit at this location, marking it for storage statement removal"
2127+
);
2128+
self.storage_to_remove.insert(local);
2129+
}
2130+
}
2131+
}

compiler/rustc_mir_transform/src/ssa.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use rustc_middle::middle::resolve_bound_vars::Set1;
1414
use rustc_middle::mir::visit::*;
1515
use rustc_middle::mir::*;
1616
use rustc_middle::ty::{self, TyCtxt};
17+
use rustc_mir_dataflow::Analysis;
1718
use tracing::{debug, instrument, trace};
1819

1920
pub(super) struct SsaLocals {
@@ -391,3 +392,64 @@ impl StorageLiveLocals {
391392
matches!(self.storage_live[local], Set1::One(_))
392393
}
393394
}
395+
396+
/// A dataflow analysis that tracks locals that are maybe uninitialized.
397+
///
398+
/// This is a simpler analysis than `MaybeUninitializedPlaces`, because it does not track
399+
/// individual fields.
400+
pub(crate) struct MaybeUninitializedLocals;
401+
402+
impl<'tcx> Analysis<'tcx> for MaybeUninitializedLocals {
403+
type Domain = DenseBitSet<Local>;
404+
405+
const NAME: &'static str = "maybe_uninit_locals";
406+
407+
fn bottom_value(&self, body: &Body<'tcx>) -> Self::Domain {
408+
// bottom = all locals are initialized.
409+
DenseBitSet::new_empty(body.local_decls.len())
410+
}
411+
412+
fn initialize_start_block(&self, body: &Body<'tcx>, state: &mut Self::Domain) {
413+
// All locals start as uninitialized...
414+
state.insert_all();
415+
// ...except for arguments, which are definitely initialized.
416+
for arg in body.args_iter() {
417+
state.remove(arg);
418+
}
419+
}
420+
421+
fn apply_primary_statement_effect(
422+
&self,
423+
state: &mut Self::Domain,
424+
statement: &Statement<'tcx>,
425+
_location: Location,
426+
) {
427+
match statement.kind {
428+
// An assignment makes a local initialized.
429+
StatementKind::Assign(box (place, _)) => {
430+
if let Some(local) = place.as_local() {
431+
state.remove(local);
432+
}
433+
}
434+
// Storage{Live,Dead} makes a local uninitialized.
435+
StatementKind::StorageLive(local) | StatementKind::StorageDead(local) => {
436+
state.insert(local);
437+
}
438+
_ => {}
439+
}
440+
}
441+
442+
fn apply_call_return_effect(
443+
&self,
444+
state: &mut Self::Domain,
445+
_block: BasicBlock,
446+
return_places: CallReturnPlaces<'_, 'tcx>,
447+
) {
448+
// The return place of a call is initialized.
449+
return_places.for_each(|place| {
450+
if let Some(local) = place.as_local() {
451+
state.remove(local);
452+
}
453+
});
454+
}
455+
}

0 commit comments

Comments
 (0)