Skip to content

Commit 814fe41

Browse files
committed
Implement MaybeUninitializedLocals analysis for copy_prop mir-opt to remove fewer storage statements
1 parent d9829c1 commit 814fe41

32 files changed

+823
-58
lines changed

compiler/rustc_mir_dataflow/src/impls/initialized.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,80 @@ 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+
for arg in body.args_iter() {
588+
state.remove(arg);
589+
}
590+
}
591+
592+
fn apply_primary_statement_effect(
593+
&mut self,
594+
state: &mut Self::Domain,
595+
statement: &mir::Statement<'tcx>,
596+
_location: Location,
597+
) {
598+
match statement.kind {
599+
// An assignment makes a local initialized.
600+
mir::StatementKind::Assign(box (place, _)) => {
601+
if let Some(local) = place.as_local() {
602+
state.remove(local);
603+
}
604+
}
605+
// Deinit makes the local uninitialized.
606+
mir::StatementKind::Deinit(box place) => {
607+
// A deinit makes a local uninitialized.
608+
if let Some(local) = place.as_local() {
609+
state.insert(local);
610+
}
611+
}
612+
// Storage{Live,Dead} makes a local uninitialized.
613+
mir::StatementKind::StorageLive(local) | mir::StatementKind::StorageDead(local) => {
614+
state.insert(local);
615+
}
616+
_ => {}
617+
}
618+
}
619+
620+
fn apply_call_return_effect(
621+
&mut self,
622+
state: &mut Self::Domain,
623+
_block: mir::BasicBlock,
624+
return_places: CallReturnPlaces<'_, 'tcx>,
625+
) {
626+
// The return place of a call is initialized.
627+
return_places.for_each(|place| {
628+
if let Some(local) = place.as_local() {
629+
state.remove(local);
630+
}
631+
});
632+
}
633+
}
634+
561635
/// There can be many more `InitIndex` than there are locals in a MIR body.
562636
/// We use a mixed bitset to avoid paying too high a memory footprint.
563637
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: 92 additions & 4 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 {
@@ -30,15 +32,30 @@ impl<'tcx> crate::MirPass<'tcx> for CopyProp {
3032

3133
let typing_env = body.typing_env(tcx);
3234
let ssa = SsaLocals::new(tcx, body, typing_env);
33-
debug!(borrowed_locals = ?ssa.borrowed_locals());
35+
let borrowed_locals = ssa.borrowed_locals().clone();
36+
37+
debug!(?borrowed_locals);
3438
debug!(copy_classes = ?ssa.copy_classes());
3539

3640
let mut any_replacement = false;
37-
let mut storage_to_remove = DenseBitSet::new_empty(body.local_decls.len());
41+
let fully_moved = fully_moved_locals(&ssa, body);
42+
debug!(?fully_moved);
43+
44+
let mut head_storage_to_check = DenseBitSet::new_empty(fully_moved.domain_size());
45+
let mut storage_to_remove = DenseBitSet::new_empty(fully_moved.domain_size());
46+
3847
for (local, &head) in ssa.copy_classes().iter_enumerated() {
3948
if local != head {
4049
any_replacement = true;
41-
storage_to_remove.insert(head);
50+
// We need to determine if we can keep the head's storage statements (which enables better optimizations).
51+
// For every local's usage location, if the head is maybe-uninitialized, we'll need to remove it's storage statements.
52+
head_storage_to_check.insert(head);
53+
54+
if borrowed_locals.contains(local) {
55+
// To keep the storage of a head, we require that none of the locals in it's copy class are borrowed,
56+
// since otherwise we cannot easily identify when it is used.
57+
storage_to_remove.insert(head);
58+
}
4259
}
4360
}
4461

@@ -49,6 +66,29 @@ impl<'tcx> crate::MirPass<'tcx> for CopyProp {
4966
let fully_moved = fully_moved_locals(&ssa, body);
5067
debug!(?fully_moved);
5168

69+
// Debug builds have no use for the storage statements, so avoid extra work.
70+
let storage_to_remove = if any_replacement && tcx.sess.emit_lifetime_markers() {
71+
let maybe_uninit = MaybeUninitializedLocals::new()
72+
.iterate_to_fixpoint(tcx, body, Some("mir_opt::copy_prop"))
73+
.into_results_cursor(body);
74+
75+
let mut storage_checker = StorageChecker {
76+
maybe_uninit,
77+
copy_classes: ssa.copy_classes(),
78+
head_storage_to_check,
79+
storage_to_remove,
80+
};
81+
82+
storage_checker.visit_body(body);
83+
84+
storage_checker.storage_to_remove
85+
} else {
86+
// Conservatively remove all storage statements for the head locals.
87+
head_storage_to_check
88+
};
89+
90+
debug!(?storage_to_remove);
91+
5292
Replacer { tcx, copy_classes: ssa.copy_classes(), fully_moved, storage_to_remove }
5393
.visit_body_preserves_cfg(body);
5494

@@ -154,3 +194,51 @@ impl<'tcx> MutVisitor<'tcx> for Replacer<'_, 'tcx> {
154194
}
155195
}
156196
}
197+
198+
// Marks heads of copy classes that are maybe uninitialized at the location of a local
199+
// as needing storage statement removal.
200+
struct StorageChecker<'a, 'tcx> {
201+
maybe_uninit: ResultsCursor<'a, 'tcx, MaybeUninitializedLocals>,
202+
copy_classes: &'a IndexSlice<Local, Local>,
203+
head_storage_to_check: DenseBitSet<Local>,
204+
storage_to_remove: DenseBitSet<Local>,
205+
}
206+
207+
impl<'a, 'tcx> Visitor<'tcx> for StorageChecker<'a, 'tcx> {
208+
fn visit_local(&mut self, local: Local, context: PlaceContext, loc: Location) {
209+
// We don't need to check storage statements and statements for which the local doesn't need to be initialized.
210+
match context {
211+
PlaceContext::MutatingUse(
212+
MutatingUseContext::Store
213+
| MutatingUseContext::Call
214+
| MutatingUseContext::Yield
215+
| MutatingUseContext::AsmOutput,
216+
)
217+
| PlaceContext::NonUse(_) => {
218+
return;
219+
}
220+
_ => {}
221+
};
222+
223+
let head = self.copy_classes[local];
224+
225+
// The head must be initialized at the location of the local, otherwise we must remove it's storage statements.
226+
if self.head_storage_to_check.contains(head) {
227+
self.maybe_uninit.seek_before_primary_effect(loc);
228+
229+
if self.maybe_uninit.get().contains(head) {
230+
debug!(
231+
?loc,
232+
?context,
233+
?local,
234+
?head,
235+
"found a head at a location in which it is maybe uninit, marking head for storage statement removal"
236+
);
237+
self.storage_to_remove.insert(head);
238+
239+
// Once we found a use of the head that is maybe uninit, we do not need to check it again.
240+
self.head_storage_to_check.remove(head);
241+
}
242+
}
243+
}
244+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
- // MIR for `f` before CopyProp
2+
+ // MIR for `f` after CopyProp
3+
4+
fn f(_1: (T, 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 = copy (_1.0: T);
13+
- _3 = copy _2;
14+
- _4 = &_3;
15+
- StorageDead(_2);
16+
+ _4 = &_2;
17+
_0 = copy (*_4);
18+
return;
19+
}
20+
}
21+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// skip-filecheck
2+
//@ test-mir-pass: CopyProp
3+
4+
#![feature(custom_mir, core_intrinsics, freeze)]
5+
6+
// Check that we remove the storage statements if one of the locals is borrowed,
7+
// and the head isn't borrowed.
8+
9+
use std::intrinsics::mir::*;
10+
use std::marker::Freeze;
11+
12+
// EMIT_MIR copy_prop_borrowed_storage_not_removed.f.CopyProp.diff
13+
14+
#[custom_mir(dialect = "runtime")]
15+
pub fn f<T: Copy + Freeze>(_1: (T, T)) -> T {
16+
mir! {
17+
let _2: T;
18+
let _3: T;
19+
let _4: &T;
20+
{
21+
StorageLive(_2);
22+
_2 = _1.0;
23+
_3 = _2;
24+
_4 = &_3;
25+
StorageDead(_2);
26+
RET = *_4;
27+
Return()
28+
}
29+
}
30+
}
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
- // MIR for `live_twice` before CopyProp
2+
+ // MIR for `live_twice` after CopyProp
3+
4+
fn live_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+
- StorageLive(_2);
18+
- _0 = opaque::<T>(copy _4) -> [return: bb2, unwind unreachable];
19+
+ _0 = opaque::<T>(copy _2) -> [return: bb2, unwind unreachable];
20+
}
21+
22+
bb2: {
23+
- StorageDead(_2);
24+
return;
25+
}
26+
}
27+
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// skip-filecheck
2+
//@ test-mir-pass: CopyProp
3+
//@ compile-flags: -Zlint-mir=false
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_twice.dead_twice.CopyProp.diff
13+
// EMIT_MIR copy_prop_storage_twice.live_twice.CopyProp.diff
14+
15+
#[custom_mir(dialect = "runtime")]
16+
pub fn dead_twice<T: Copy>(_1: T) -> T {
17+
mir! {
18+
let _2: T;
19+
let _3: T;
20+
{
21+
StorageLive(_2);
22+
Call(_2 = opaque(Move(_1)), ReturnTo(bb1), UnwindUnreachable())
23+
}
24+
bb1 = {
25+
let _3 = Move(_2);
26+
StorageDead(_2);
27+
StorageLive(_2);
28+
Call(RET = opaque(Move(_3)), ReturnTo(bb2), UnwindUnreachable())
29+
}
30+
bb2 = {
31+
StorageDead(_2);
32+
Return()
33+
}
34+
}
35+
}
36+
37+
#[custom_mir(dialect = "runtime")]
38+
pub fn live_twice<T: Copy>(_1: T) -> T {
39+
mir! {
40+
let _2: T;
41+
let _3: T;
42+
{
43+
StorageLive(_2);
44+
Call(_2 = opaque(Move(_1)), ReturnTo(bb1), UnwindUnreachable())
45+
}
46+
bb1 = {
47+
let _3 = Move(_2);
48+
StorageLive(_2);
49+
Call(RET = opaque(_3), ReturnTo(bb2), UnwindUnreachable())
50+
}
51+
bb2 = {
52+
StorageDead(_2);
53+
Return()
54+
}
55+
}
56+
}
57+
58+
#[inline(never)]
59+
fn opaque<T>(a: T) -> T {
60+
a
61+
}

0 commit comments

Comments
 (0)