Skip to content

Commit 50dff95

Browse files
committed
Auto merge of rust-lang#106285 - cjgillot:refprop-ssa, r=JakobDegen
Implement SSA-based reference propagation Rust has a tendency to create a lot of short-lived borrows, in particular for method calls. This PR aims to remove those short-lived borrows with a const-propagation dedicated to pointers to local places. This pass aims to transform the following pattern: ``` _1 = &raw? mut? PLACE; _3 = *_1; _4 = &raw? mut? *_1; ``` Into ``` _1 = &raw? mut? PLACE; _3 = PLACE; _4 = &raw? mut? PLACE; ``` where `PLACE` is a direct or an indirect place expression. By removing indirection, this pass should help both dest-prop and const-prop to handle more cases. This optimization is distinct from const-prop and dataflow const-prop since the borrow-reborrow patterns needs to preserve borrowck invariants, especially the uniqueness property of mutable references. The pointed-to places are computed using a SSA analysis. We suppose that removable borrows are typically temporaries from autoref, so they are by construction assigned only once, and a SSA analysis is enough to catch them. For each local, we store both where and how it is used, in order to efficiently compute the all-or-nothing property. Thanks to `Derefer`, we only have to track locals, not places in general. --- There are 3 properties that need to be upheld for this transformation to be legal: - place constness: `PLACE` must refer to the same memory wherever it appears; - pointer liveness: we must not introduce dereferences of dangling pointers; - `&mut` borrow uniqueness. ## Constness If `PLACE` is an indirect projection, if its of the form `(*LOCAL).PROJECTIONS` where: - `LOCAL` is SSA; - all projections in `PROJECTIONS` are constant (no dereference and no indexing). If `PLACE` is a direct projection of a local, we consider it as constant if: - the local is always live, or it has a single `StorageLive` that dominates all uses; - all projections are constant. # Liveness When performing a substitution, we must take care not to introduce uses of dangling locals. Using a dangling borrow is UB. Therefore, we assume that for any use of `*x`, where `x` is a borrow, the pointed-to memory is live. Limitations: - occurrences of `*x` in an `&raw mut? *x` are accepted; - raw pointers are allowed to be dangling. In those 2 case, we do not substitute anything, to be on the safe side. **Open question:** we do not differentiate borrows of ZST and non-ZST. The UB rules may be different depending on the layout. Having a different treatment would effectively prevent this pass from running on polymorphic MIR, which defeats the purpose of MIR opts. ## Uniqueness For `&mut` borrows, we also need to preserve the uniqueness property: we must avoid creating a state where we interleave uses of `*_1` and `_2`. To do it, we only perform full substitution of mutable borrows: we replace either all or none of the occurrences of `*_1`. Some care has to be taken when `_1` is copied in other locals. ``` _1 = &raw? mut? _2; _3 = *_1; _4 = _1 _5 = *_4 ``` In such cases, fully substituting `_1` means fully substituting all of the copies. For immutable borrows, we do not need to preserve such uniqueness property, so we perform all the possible substitutions without removing the `_1 = &_2` statement.
2 parents 2f6bc5d + bde213c commit 50dff95

23 files changed

+3141
-115
lines changed

compiler/rustc_middle/src/mir/mod.rs

+13
Original file line numberDiff line numberDiff line change
@@ -1524,6 +1524,19 @@ impl<V, T> ProjectionElem<V, T> {
15241524
}
15251525
}
15261526

1527+
/// Returns `true` if the target of this projection always refers to the same memory region
1528+
/// whatever the state of the program.
1529+
pub fn is_stable_offset(&self) -> bool {
1530+
match self {
1531+
Self::Deref | Self::Index(_) => false,
1532+
Self::Field(_, _)
1533+
| Self::OpaqueCast(_)
1534+
| Self::ConstantIndex { .. }
1535+
| Self::Subslice { .. }
1536+
| Self::Downcast(_, _) => true,
1537+
}
1538+
}
1539+
15271540
/// Returns `true` if this is a `Downcast` projection with the given `VariantIdx`.
15281541
pub fn is_downcast_to(&self, v: VariantIdx) -> bool {
15291542
matches!(*self, Self::Downcast(_, x) if x == v)

compiler/rustc_mir_dataflow/src/impls/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub use self::borrowed_locals::borrowed_locals;
2626
pub use self::borrowed_locals::MaybeBorrowedLocals;
2727
pub use self::liveness::MaybeLiveLocals;
2828
pub use self::liveness::MaybeTransitiveLiveLocals;
29-
pub use self::storage_liveness::{MaybeRequiresStorage, MaybeStorageLive};
29+
pub use self::storage_liveness::{MaybeRequiresStorage, MaybeStorageDead, MaybeStorageLive};
3030

3131
/// `MaybeInitializedPlaces` tracks all places that might be
3232
/// initialized upon reaching a particular point in the control flow

compiler/rustc_mir_dataflow/src/impls/storage_liveness.rs

+67
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,73 @@ impl<'tcx, 'a> crate::GenKillAnalysis<'tcx> for MaybeStorageLive<'a> {
7474
}
7575
}
7676

77+
#[derive(Clone)]
78+
pub struct MaybeStorageDead {
79+
always_live_locals: BitSet<Local>,
80+
}
81+
82+
impl MaybeStorageDead {
83+
pub fn new(always_live_locals: BitSet<Local>) -> Self {
84+
MaybeStorageDead { always_live_locals }
85+
}
86+
}
87+
88+
impl<'tcx> crate::AnalysisDomain<'tcx> for MaybeStorageDead {
89+
type Domain = BitSet<Local>;
90+
91+
const NAME: &'static str = "maybe_storage_dead";
92+
93+
fn bottom_value(&self, body: &mir::Body<'tcx>) -> Self::Domain {
94+
// bottom = live
95+
BitSet::new_empty(body.local_decls.len())
96+
}
97+
98+
fn initialize_start_block(&self, body: &mir::Body<'tcx>, on_entry: &mut Self::Domain) {
99+
assert_eq!(body.local_decls.len(), self.always_live_locals.domain_size());
100+
// Do not iterate on return place and args, as they are trivially always live.
101+
for local in body.vars_and_temps_iter() {
102+
if !self.always_live_locals.contains(local) {
103+
on_entry.insert(local);
104+
}
105+
}
106+
}
107+
}
108+
109+
impl<'tcx> crate::GenKillAnalysis<'tcx> for MaybeStorageDead {
110+
type Idx = Local;
111+
112+
fn statement_effect(
113+
&self,
114+
trans: &mut impl GenKill<Self::Idx>,
115+
stmt: &mir::Statement<'tcx>,
116+
_: Location,
117+
) {
118+
match stmt.kind {
119+
StatementKind::StorageLive(l) => trans.kill(l),
120+
StatementKind::StorageDead(l) => trans.gen(l),
121+
_ => (),
122+
}
123+
}
124+
125+
fn terminator_effect(
126+
&self,
127+
_trans: &mut impl GenKill<Self::Idx>,
128+
_: &mir::Terminator<'tcx>,
129+
_: Location,
130+
) {
131+
// Terminators have no effect
132+
}
133+
134+
fn call_return_effect(
135+
&self,
136+
_trans: &mut impl GenKill<Self::Idx>,
137+
_block: BasicBlock,
138+
_return_places: CallReturnPlaces<'_, 'tcx>,
139+
) {
140+
// Nothing to do when a call returns successfully
141+
}
142+
}
143+
77144
type BorrowedLocalsResults<'a, 'tcx> = ResultsRefCursor<'a, 'a, 'tcx, MaybeBorrowedLocals>;
78145

79146
/// Dataflow analysis that determines whether each local requires storage at a

compiler/rustc_mir_transform/src/copy_prop.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,8 @@ impl<'tcx> MirPass<'tcx> for CopyProp {
3333
}
3434

3535
fn propagate_ssa<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
36-
let param_env = tcx.param_env_reveal_all_normalized(body.source.def_id());
3736
let borrowed_locals = borrowed_locals(body);
38-
let ssa = SsaLocals::new(tcx, param_env, body, &borrowed_locals);
37+
let ssa = SsaLocals::new(body);
3938

4039
let fully_moved = fully_moved_locals(&ssa, body);
4140
debug!(?fully_moved);
@@ -76,7 +75,7 @@ fn propagate_ssa<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
7675
fn fully_moved_locals(ssa: &SsaLocals, body: &Body<'_>) -> BitSet<Local> {
7776
let mut fully_moved = BitSet::new_filled(body.local_decls.len());
7877

79-
for (_, rvalue) in ssa.assignments(body) {
78+
for (_, rvalue, _) in ssa.assignments(body) {
8079
let (Rvalue::Use(Operand::Copy(place) | Operand::Move(place)) | Rvalue::CopyForDeref(place))
8180
= rvalue
8281
else { continue };

compiler/rustc_mir_transform/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ mod match_branches;
8484
mod multiple_return_terminators;
8585
mod normalize_array_len;
8686
mod nrvo;
87+
mod ref_prop;
8788
mod remove_noop_landing_pads;
8889
mod remove_storage_markers;
8990
mod remove_uninit_drops;
@@ -559,6 +560,7 @@ fn run_optimization_passes<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
559560
&separate_const_switch::SeparateConstSwitch,
560561
&simplify::SimplifyLocals::BeforeConstProp,
561562
&copy_prop::CopyProp,
563+
&ref_prop::ReferencePropagation,
562564
&const_prop::ConstProp,
563565
&dataflow_const_prop::DataflowConstProp,
564566
//

compiler/rustc_mir_transform/src/normalize_array_len.rs

+2-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use rustc_index::IndexVec;
77
use rustc_middle::mir::visit::*;
88
use rustc_middle::mir::*;
99
use rustc_middle::ty::{self, TyCtxt};
10-
use rustc_mir_dataflow::impls::borrowed_locals;
1110

1211
pub struct NormalizeArrayLen;
1312

@@ -24,9 +23,7 @@ impl<'tcx> MirPass<'tcx> for NormalizeArrayLen {
2423
}
2524

2625
fn normalize_array_len_calls<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
27-
let param_env = tcx.param_env_reveal_all_normalized(body.source.def_id());
28-
let borrowed_locals = borrowed_locals(body);
29-
let ssa = SsaLocals::new(tcx, param_env, body, &borrowed_locals);
26+
let ssa = SsaLocals::new(body);
3027

3128
let slice_lengths = compute_slice_length(tcx, &ssa, body);
3229
debug!(?slice_lengths);
@@ -41,7 +38,7 @@ fn compute_slice_length<'tcx>(
4138
) -> IndexVec<Local, Option<ty::Const<'tcx>>> {
4239
let mut slice_lengths = IndexVec::from_elem(None, &body.local_decls);
4340

44-
for (local, rvalue) in ssa.assignments(body) {
41+
for (local, rvalue, _) in ssa.assignments(body) {
4542
match rvalue {
4643
Rvalue::Cast(
4744
CastKind::Pointer(ty::adjustment::PointerCast::Unsize),

0 commit comments

Comments
 (0)