Skip to content

Commit ad0ab67

Browse files
committed
Implement MaybeUninitializedLocals analysis for copy_prop mir-opt to remove fewer storage statements
1 parent 9df91d2 commit ad0ab67

29 files changed

+638
-46
lines changed

compiler/rustc_mir_dataflow/src/impls/initialized.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,81 @@ impl<'tcx> Analysis<'tcx> for MaybeUninitializedPlaces<'_, 'tcx> {
558558
}
559559
}
560560

561+
/// A dataflow analysis that tracks locals that are maybe uninitialized.
562+
///
563+
/// This is a simpler analysis than `MaybeUninitializedPlaces`, because it does not track
564+
/// individual fields.
565+
pub struct MaybeUninitializedLocals;
566+
567+
impl MaybeUninitializedLocals {
568+
pub fn new() -> Self {
569+
Self {}
570+
}
571+
}
572+
573+
impl<'tcx> Analysis<'tcx> for MaybeUninitializedLocals {
574+
type Domain = DenseBitSet<mir::Local>;
575+
576+
const NAME: &'static str = "maybe_uninit_locals";
577+
578+
fn bottom_value(&self, body: &Body<'tcx>) -> Self::Domain {
579+
// bottom = all locals are initialized.
580+
DenseBitSet::new_empty(body.local_decls.len())
581+
}
582+
583+
fn initialize_start_block(&self, body: &Body<'tcx>, state: &mut Self::Domain) {
584+
// All locals start as uninitialized...
585+
state.insert_all();
586+
// ...except for arguments, which are definitely initialized.
587+
// Local 0 is the return place, which is uninitialized.
588+
for arg in body.args_iter() {
589+
state.remove(arg);
590+
}
591+
}
592+
593+
fn apply_primary_statement_effect(
594+
&mut self,
595+
state: &mut Self::Domain,
596+
statement: &mir::Statement<'tcx>,
597+
_location: Location,
598+
) {
599+
match statement.kind {
600+
// An assignment makes a local initialized.
601+
mir::StatementKind::Assign(box (place, _)) => {
602+
if let Some(local) = place.as_local() {
603+
state.remove(local);
604+
}
605+
}
606+
// Deinit makes the local uninitialized.
607+
mir::StatementKind::Deinit(box place) => {
608+
// A deinit makes a local uninitialized.
609+
if let Some(local) = place.as_local() {
610+
state.insert(local);
611+
}
612+
}
613+
// StorageDead makes a local uninitialized.
614+
mir::StatementKind::StorageDead(local) => {
615+
state.insert(local);
616+
}
617+
_ => {}
618+
}
619+
}
620+
621+
fn apply_call_return_effect(
622+
&mut self,
623+
state: &mut Self::Domain,
624+
_block: mir::BasicBlock,
625+
return_places: CallReturnPlaces<'_, 'tcx>,
626+
) {
627+
// The return place of a call is initialized.
628+
return_places.for_each(|place| {
629+
if let Some(local) = place.as_local() {
630+
state.remove(local);
631+
}
632+
});
633+
}
634+
}
635+
561636
/// There can be many more `InitIndex` than there are locals in a MIR body.
562637
/// We use a mixed bitset to avoid paying too high a memory footprint.
563638
pub type EverInitializedPlacesDomain = MixedBitSet<InitIndex>;

compiler/rustc_mir_dataflow/src/impls/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ mod storage_liveness;
66
pub use self::borrowed_locals::{MaybeBorrowedLocals, borrowed_locals};
77
pub use self::initialized::{
88
EverInitializedPlaces, EverInitializedPlacesDomain, MaybeInitializedPlaces,
9-
MaybeUninitializedPlaces, MaybeUninitializedPlacesDomain,
9+
MaybeUninitializedLocals, MaybeUninitializedPlaces, MaybeUninitializedPlacesDomain,
1010
};
1111
pub use self::liveness::{
1212
MaybeLiveLocals, MaybeTransitiveLiveLocals, TransferFunction as LivenessTransferFunction,

compiler/rustc_mir_transform/src/copy_prop.rs

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ 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::impls::MaybeUninitializedLocals;
7+
use rustc_mir_dataflow::{Analysis, ResultsCursor};
68
use tracing::{debug, instrument};
79

810
use crate::ssa::SsaLocals;
@@ -16,7 +18,7 @@ use crate::ssa::SsaLocals;
1618
/// _d = move? _c
1719
/// where each of the locals is only assigned once.
1820
///
19-
/// We want to replace all those locals by `_a`, either copied or moved.
21+
/// We want to replace all those locals by `_a` (the "head"), either copied or moved.
2022
pub(super) struct CopyProp;
2123

2224
impl<'tcx> crate::MirPass<'tcx> for CopyProp {
@@ -34,25 +36,49 @@ impl<'tcx> crate::MirPass<'tcx> for CopyProp {
3436
let fully_moved = fully_moved_locals(&ssa, body);
3537
debug!(?fully_moved);
3638

37-
let mut storage_to_remove = DenseBitSet::new_empty(fully_moved.domain_size());
39+
let mut head_storage_to_check = DenseBitSet::new_empty(fully_moved.domain_size());
40+
3841
for (local, &head) in ssa.copy_classes().iter_enumerated() {
3942
if local != head {
40-
storage_to_remove.insert(head);
43+
// We need to determine if we can keep the head's storage statements (which enables better optimizations).
44+
// For every local's usage location, if the head is maybe-uninitialized, we'll need to remove it's storage statements.
45+
head_storage_to_check.insert(head);
4146
}
4247
}
4348

4449
let any_replacement = ssa.copy_classes().iter_enumerated().any(|(l, &h)| l != h);
4550

51+
let storage_to_remove = DenseBitSet::new_empty(fully_moved.domain_size());
52+
4653
Replacer {
4754
tcx,
4855
copy_classes: ssa.copy_classes(),
4956
fully_moved,
5057
borrowed_locals: ssa.borrowed_locals(),
51-
storage_to_remove,
5258
}
5359
.visit_body_preserves_cfg(body);
5460

61+
debug!(?head_storage_to_check);
62+
5563
if any_replacement {
64+
// Debug builds have no use for the storage statements, so avoid extra work.
65+
let storage_to_remove = if tcx.sess.emit_lifetime_markers() {
66+
let maybe_uninit = MaybeUninitializedLocals::new()
67+
.iterate_to_fixpoint(tcx, body, Some("mir_opt::copy_prop"))
68+
.into_results_cursor(body);
69+
70+
let mut storage_checker =
71+
StorageChecker { maybe_uninit, head_storage_to_check, storage_to_remove };
72+
73+
storage_checker.visit_body(body);
74+
75+
storage_checker.storage_to_remove
76+
} else {
77+
// Conservatively remove all storage statements for the head locals.
78+
head_storage_to_check
79+
};
80+
StorageRemover { tcx, storage_to_remove }.visit_body_preserves_cfg(body);
81+
5682
crate::simplify::remove_unused_definitions(body);
5783
}
5884
}
@@ -101,7 +127,6 @@ fn fully_moved_locals(ssa: &SsaLocals, body: &Body<'_>) -> DenseBitSet<Local> {
101127
struct Replacer<'a, 'tcx> {
102128
tcx: TyCtxt<'tcx>,
103129
fully_moved: DenseBitSet<Local>,
104-
storage_to_remove: DenseBitSet<Local>,
105130
borrowed_locals: &'a DenseBitSet<Local>,
106131
copy_classes: &'a IndexSlice<Local, Local>,
107132
}
@@ -119,6 +144,7 @@ impl<'tcx> MutVisitor<'tcx> for Replacer<'_, 'tcx> {
119144
if self.borrowed_locals.contains(*local) {
120145
return;
121146
}
147+
122148
match ctxt {
123149
// Do not modify the local in storage statements.
124150
PlaceContext::NonUse(NonUseContext::StorageLive | NonUseContext::StorageDead) => {}
@@ -152,14 +178,6 @@ impl<'tcx> MutVisitor<'tcx> for Replacer<'_, 'tcx> {
152178
}
153179

154180
fn visit_statement(&mut self, stmt: &mut Statement<'tcx>, loc: Location) {
155-
// When removing storage statements, we need to remove both (#107511).
156-
if let StatementKind::StorageLive(l) | StatementKind::StorageDead(l) = stmt.kind
157-
&& self.storage_to_remove.contains(l)
158-
{
159-
stmt.make_nop();
160-
return;
161-
}
162-
163181
self.super_statement(stmt, loc);
164182

165183
// Do not leave tautological assignments around.
@@ -172,3 +190,67 @@ impl<'tcx> MutVisitor<'tcx> for Replacer<'_, 'tcx> {
172190
}
173191
}
174192
}
193+
194+
struct StorageChecker<'a, 'tcx> {
195+
storage_to_remove: DenseBitSet<Local>,
196+
head_storage_to_check: DenseBitSet<Local>,
197+
maybe_uninit: ResultsCursor<'a, 'tcx, MaybeUninitializedLocals>,
198+
}
199+
200+
impl<'a, 'tcx> Visitor<'tcx> for StorageChecker<'a, 'tcx> {
201+
fn visit_local(&mut self, local: Local, context: PlaceContext, loc: Location) {
202+
// We don't need to check storage statements and statements for which the local doesn't need to be initialized.
203+
match context {
204+
PlaceContext::MutatingUse(
205+
MutatingUseContext::Store
206+
| MutatingUseContext::Call
207+
| MutatingUseContext::AsmOutput,
208+
)
209+
| PlaceContext::NonUse(_) => {
210+
return;
211+
}
212+
_ => {}
213+
};
214+
215+
// The head must be initialized at the location of the local, otherwise we must remove it's storage statements.
216+
if self.head_storage_to_check.contains(local) {
217+
self.maybe_uninit.seek_before_primary_effect(loc);
218+
219+
if self.maybe_uninit.get().contains(local) {
220+
debug!(
221+
?loc,
222+
?context,
223+
?local,
224+
"found a head at a location in which it is maybe uninit, marking head for storage statement removal"
225+
);
226+
self.storage_to_remove.insert(local);
227+
228+
// Once we found a use of the head that is maybe uninit, we do not need to check it again.
229+
self.head_storage_to_check.remove(local);
230+
}
231+
}
232+
}
233+
}
234+
235+
struct StorageRemover<'tcx> {
236+
tcx: TyCtxt<'tcx>,
237+
storage_to_remove: DenseBitSet<Local>,
238+
}
239+
240+
impl<'tcx> MutVisitor<'tcx> for StorageRemover<'tcx> {
241+
fn tcx(&self) -> TyCtxt<'tcx> {
242+
self.tcx
243+
}
244+
245+
fn visit_statement(&mut self, stmt: &mut Statement<'tcx>, loc: Location) {
246+
// When removing storage statements, we need to remove both (#107511).
247+
if let StatementKind::StorageLive(l) | StatementKind::StorageDead(l) = stmt.kind
248+
&& self.storage_to_remove.contains(l)
249+
{
250+
stmt.make_nop();
251+
return;
252+
}
253+
254+
self.super_statement(stmt, loc);
255+
}
256+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
- // MIR for `dead_twice` before CopyProp
2+
+ // MIR for `dead_twice` after CopyProp
3+
4+
fn dead_twice(_1: T) -> T {
5+
let mut _0: T;
6+
let mut _2: T;
7+
let mut _3: T;
8+
let mut _4: T;
9+
10+
bb0: {
11+
- StorageLive(_2);
12+
_2 = opaque::<T>(move _1) -> [return: bb1, unwind unreachable];
13+
}
14+
15+
bb1: {
16+
- _4 = move _2;
17+
- StorageDead(_2);
18+
- StorageLive(_2);
19+
- _0 = opaque::<T>(move _4) -> [return: bb2, unwind unreachable];
20+
+ _0 = opaque::<T>(move _2) -> [return: bb2, unwind unreachable];
21+
}
22+
23+
bb2: {
24+
- StorageDead(_2);
25+
return;
26+
}
27+
}
28+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
- // MIR for `dead_twice` before CopyProp
2+
+ // MIR for `dead_twice` after CopyProp
3+
4+
fn dead_twice(_1: T) -> T {
5+
let mut _0: T;
6+
let mut _2: T;
7+
let mut _3: T;
8+
let mut _4: T;
9+
10+
bb0: {
11+
- StorageLive(_2);
12+
_2 = opaque::<T>(move _1) -> [return: bb1, unwind unreachable];
13+
}
14+
15+
bb1: {
16+
- _4 = move _2;
17+
- StorageDead(_2);
18+
- StorageLive(_2);
19+
- _0 = opaque::<T>(move _4) -> [return: bb2, unwind unreachable];
20+
+ _0 = opaque::<T>(move _2) -> [return: bb2, unwind unreachable];
21+
}
22+
23+
bb2: {
24+
- StorageDead(_2);
25+
return;
26+
}
27+
}
28+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// skip-filecheck
2+
// EMIT_MIR_FOR_EACH_PANIC_STRATEGY
3+
//@ test-mir-pass: CopyProp
4+
5+
#![feature(custom_mir, core_intrinsics)]
6+
7+
// Check that we remove the storage statements if the head
8+
// becomes uninitialized before it is used again.
9+
10+
use std::intrinsics::mir::*;
11+
12+
// EMIT_MIR copy_prop_storage_dead_twice.dead_twice.CopyProp.diff
13+
#[custom_mir(dialect = "runtime")]
14+
pub fn dead_twice<T: Copy>(_1: T) -> T {
15+
mir! {
16+
let _2: T;
17+
let _3: T;
18+
{
19+
StorageLive(_2);
20+
Call(_2 = opaque(Move(_1)), ReturnTo(bb1), UnwindUnreachable())
21+
}
22+
bb1 = {
23+
let _3 = Move(_2);
24+
StorageDead(_2);
25+
StorageLive(_2);
26+
Call(RET = opaque(Move(_3)), ReturnTo(bb2), UnwindUnreachable())
27+
}
28+
bb2 = {
29+
StorageDead(_2);
30+
Return()
31+
}
32+
}
33+
}
34+
35+
#[inline(never)]
36+
fn opaque<T>(a: T) -> T {
37+
a
38+
}

tests/mir-opt/copy-prop/cycle.main.CopyProp.panic-abort.diff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
}
2727

2828
bb1: {
29-
- StorageLive(_2);
29+
StorageLive(_2);
3030
_2 = copy _1;
3131
- StorageLive(_3);
3232
- _3 = copy _2;
@@ -46,7 +46,7 @@
4646
StorageDead(_5);
4747
_0 = const ();
4848
- StorageDead(_3);
49-
- StorageDead(_2);
49+
StorageDead(_2);
5050
StorageDead(_1);
5151
return;
5252
}

tests/mir-opt/copy-prop/cycle.main.CopyProp.panic-unwind.diff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
}
2727

2828
bb1: {
29-
- StorageLive(_2);
29+
StorageLive(_2);
3030
_2 = copy _1;
3131
- StorageLive(_3);
3232
- _3 = copy _2;
@@ -46,7 +46,7 @@
4646
StorageDead(_5);
4747
_0 = const ();
4848
- StorageDead(_3);
49-
- StorageDead(_2);
49+
StorageDead(_2);
5050
StorageDead(_1);
5151
return;
5252
}

0 commit comments

Comments
 (0)