Skip to content

Commit 002bc18

Browse files
committed
Implement MaybeUninitializedLocals analysis for copy_prop mir-opt to remove fewer storage statements
1 parent c84c657 commit 002bc18

File tree

46 files changed

+856
-104
lines changed

Some content is hidden

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

46 files changed

+856
-104
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: 88 additions & 3 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,21 +32,56 @@ 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 fully_moved = fully_moved_locals(&ssa, body);
3741
debug!(?fully_moved);
3842

43+
let mut head_storage_to_check = DenseBitSet::new_empty(fully_moved.domain_size());
3944
let mut storage_to_remove = DenseBitSet::new_empty(fully_moved.domain_size());
45+
4046
for (local, &head) in ssa.copy_classes().iter_enumerated() {
4147
if local != head {
42-
storage_to_remove.insert(head);
48+
// We need to determine if we can keep the head's storage statements (which enables better optimizations).
49+
// For every local's usage location, if the head is maybe-uninitialized, we'll need to remove it's storage statements.
50+
head_storage_to_check.insert(head);
51+
52+
if borrowed_locals.contains(local) {
53+
// To keep the storage of a head, we require that none of the locals in it's copy class are borrowed,
54+
// since otherwise we cannot easily identify when it is used.
55+
storage_to_remove.insert(head);
56+
}
4357
}
4458
}
4559

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

62+
// Debug builds have no use for the storage statements, so avoid extra work.
63+
let storage_to_remove = if any_replacement && tcx.sess.emit_lifetime_markers() {
64+
let maybe_uninit = MaybeUninitializedLocals::new()
65+
.iterate_to_fixpoint(tcx, body, Some("mir_opt::copy_prop"))
66+
.into_results_cursor(body);
67+
68+
let mut storage_checker = StorageChecker {
69+
maybe_uninit,
70+
copy_classes: ssa.copy_classes(),
71+
head_storage_to_check,
72+
storage_to_remove,
73+
};
74+
75+
storage_checker.visit_body(body);
76+
77+
storage_checker.storage_to_remove
78+
} else {
79+
// Conservatively remove all storage statements for the head locals.
80+
head_storage_to_check
81+
};
82+
83+
debug!(?storage_to_remove);
84+
4885
Replacer { tcx, copy_classes: ssa.copy_classes(), fully_moved, storage_to_remove }
4986
.visit_body_preserves_cfg(body);
5087

@@ -152,3 +189,51 @@ impl<'tcx> MutVisitor<'tcx> for Replacer<'_, 'tcx> {
152189
}
153190
}
154191
}
192+
193+
// Marks heads of copy classes that are maybe uninitialized at the location of a local
194+
// as needing storage statement removal.
195+
struct StorageChecker<'a, 'tcx> {
196+
maybe_uninit: ResultsCursor<'a, 'tcx, MaybeUninitializedLocals>,
197+
copy_classes: &'a IndexSlice<Local, Local>,
198+
head_storage_to_check: DenseBitSet<Local>,
199+
storage_to_remove: DenseBitSet<Local>,
200+
}
201+
202+
impl<'a, 'tcx> Visitor<'tcx> for StorageChecker<'a, 'tcx> {
203+
fn visit_local(&mut self, local: Local, context: PlaceContext, loc: Location) {
204+
// We don't need to check storage statements and statements for which the local doesn't need to be initialized.
205+
match context {
206+
PlaceContext::MutatingUse(
207+
MutatingUseContext::Store
208+
| MutatingUseContext::Call
209+
| MutatingUseContext::Yield
210+
| MutatingUseContext::AsmOutput,
211+
)
212+
| PlaceContext::NonUse(_) => {
213+
return;
214+
}
215+
_ => {}
216+
};
217+
218+
let head = self.copy_classes[local];
219+
220+
// The head must be initialized at the location of the local, otherwise we must remove it's storage statements.
221+
if self.head_storage_to_check.contains(head) {
222+
self.maybe_uninit.seek_before_primary_effect(loc);
223+
224+
if self.maybe_uninit.get().contains(head) {
225+
debug!(
226+
?loc,
227+
?context,
228+
?local,
229+
?head,
230+
"found a head at a location in which it is maybe uninit, marking head for storage statement removal"
231+
);
232+
self.storage_to_remove.insert(head);
233+
234+
// Once we found a use of the head that is maybe uninit, we do not need to check it again.
235+
self.head_storage_to_check.remove(head);
236+
}
237+
}
238+
}
239+
}
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)