Skip to content

Commit 2a4b00b

Browse files
committed
Auto merge of #106908 - cjgillot:copyprop-ssa, r=oli-obk
Implement simple CopyPropagation based on SSA analysis This PR extracts the "copy propagation" logic from #106285. MIR may produce chains of assignment between locals, like `_x = move? _y`. This PR attempts to remove such chains by unifying locals. The current implementation is a bit overzealous in turning moves into copies, and in removing storage statements.
2 parents d117135 + 263da25 commit 2a4b00b

File tree

68 files changed

+1948
-278
lines changed

Some content is hidden

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

68 files changed

+1948
-278
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
use rustc_index::bit_set::BitSet;
2+
use rustc_index::vec::IndexVec;
3+
use rustc_middle::mir::visit::*;
4+
use rustc_middle::mir::*;
5+
use rustc_middle::ty::TyCtxt;
6+
use rustc_mir_dataflow::impls::borrowed_locals;
7+
8+
use crate::ssa::SsaLocals;
9+
use crate::MirPass;
10+
11+
/// Unify locals that copy each other.
12+
///
13+
/// We consider patterns of the form
14+
/// _a = rvalue
15+
/// _b = move? _a
16+
/// _c = move? _a
17+
/// _d = move? _c
18+
/// where each of the locals is only assigned once.
19+
///
20+
/// We want to replace all those locals by `_a`, either copied or moved.
21+
pub struct CopyProp;
22+
23+
impl<'tcx> MirPass<'tcx> for CopyProp {
24+
fn is_enabled(&self, sess: &rustc_session::Session) -> bool {
25+
sess.mir_opt_level() >= 4
26+
}
27+
28+
#[instrument(level = "trace", skip(self, tcx, body))]
29+
fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
30+
debug!(def_id = ?body.source.def_id());
31+
propagate_ssa(tcx, body);
32+
}
33+
}
34+
35+
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());
37+
let borrowed_locals = borrowed_locals(body);
38+
let ssa = SsaLocals::new(tcx, param_env, body, &borrowed_locals);
39+
40+
let fully_moved = fully_moved_locals(&ssa, body);
41+
debug!(?fully_moved);
42+
43+
let mut storage_to_remove = BitSet::new_empty(fully_moved.domain_size());
44+
for (local, &head) in ssa.copy_classes().iter_enumerated() {
45+
if local != head {
46+
storage_to_remove.insert(head);
47+
}
48+
}
49+
50+
let any_replacement = ssa.copy_classes().iter_enumerated().any(|(l, &h)| l != h);
51+
52+
Replacer {
53+
tcx,
54+
copy_classes: &ssa.copy_classes(),
55+
fully_moved,
56+
borrowed_locals,
57+
storage_to_remove,
58+
}
59+
.visit_body_preserves_cfg(body);
60+
61+
if any_replacement {
62+
crate::simplify::remove_unused_definitions(body);
63+
}
64+
}
65+
66+
/// `SsaLocals` computed equivalence classes between locals considering copy/move assignments.
67+
///
68+
/// This function also returns whether all the `move?` in the pattern are `move` and not copies.
69+
/// A local which is in the bitset can be replaced by `move _a`. Otherwise, it must be
70+
/// replaced by `copy _a`, as we cannot move multiple times from `_a`.
71+
///
72+
/// If an operand copies `_c`, it must happen before the assignment `_d = _c`, otherwise it is UB.
73+
/// This means that replacing it by a copy of `_a` if ok, since this copy happens before `_c` is
74+
/// moved, and therefore that `_d` is moved.
75+
#[instrument(level = "trace", skip(ssa, body))]
76+
fn fully_moved_locals(ssa: &SsaLocals, body: &Body<'_>) -> BitSet<Local> {
77+
let mut fully_moved = BitSet::new_filled(body.local_decls.len());
78+
79+
for (_, rvalue) in ssa.assignments(body) {
80+
let (Rvalue::Use(Operand::Copy(place) | Operand::Move(place)) | Rvalue::CopyForDeref(place))
81+
= rvalue
82+
else { continue };
83+
84+
let Some(rhs) = place.as_local() else { continue };
85+
if !ssa.is_ssa(rhs) {
86+
continue;
87+
}
88+
89+
if let Rvalue::Use(Operand::Copy(_)) | Rvalue::CopyForDeref(_) = rvalue {
90+
fully_moved.remove(rhs);
91+
}
92+
}
93+
94+
ssa.meet_copy_equivalence(&mut fully_moved);
95+
96+
fully_moved
97+
}
98+
99+
/// Utility to help performing subtitution of `*pattern` by `target`.
100+
struct Replacer<'a, 'tcx> {
101+
tcx: TyCtxt<'tcx>,
102+
fully_moved: BitSet<Local>,
103+
storage_to_remove: BitSet<Local>,
104+
borrowed_locals: BitSet<Local>,
105+
copy_classes: &'a IndexVec<Local, Local>,
106+
}
107+
108+
impl<'tcx> MutVisitor<'tcx> for Replacer<'_, 'tcx> {
109+
fn tcx(&self) -> TyCtxt<'tcx> {
110+
self.tcx
111+
}
112+
113+
fn visit_local(&mut self, local: &mut Local, ctxt: PlaceContext, _: Location) {
114+
let new_local = self.copy_classes[*local];
115+
match ctxt {
116+
// Do not modify the local in storage statements.
117+
PlaceContext::NonUse(NonUseContext::StorageLive | NonUseContext::StorageDead) => {}
118+
// The local should have been marked as non-SSA.
119+
PlaceContext::MutatingUse(_) => assert_eq!(*local, new_local),
120+
// We access the value.
121+
_ => *local = new_local,
122+
}
123+
}
124+
125+
fn visit_place(&mut self, place: &mut Place<'tcx>, ctxt: PlaceContext, loc: Location) {
126+
if let Some(new_projection) = self.process_projection(&place.projection, loc) {
127+
place.projection = self.tcx().intern_place_elems(&new_projection);
128+
}
129+
130+
let observes_address = match ctxt {
131+
PlaceContext::NonMutatingUse(
132+
NonMutatingUseContext::SharedBorrow
133+
| NonMutatingUseContext::ShallowBorrow
134+
| NonMutatingUseContext::UniqueBorrow
135+
| NonMutatingUseContext::AddressOf,
136+
) => true,
137+
// For debuginfo, merging locals is ok.
138+
PlaceContext::NonUse(NonUseContext::VarDebugInfo) => {
139+
self.borrowed_locals.contains(place.local)
140+
}
141+
_ => false,
142+
};
143+
if observes_address && !place.is_indirect() {
144+
// We observe the address of `place.local`. Do not replace it.
145+
} else {
146+
self.visit_local(
147+
&mut place.local,
148+
PlaceContext::NonMutatingUse(NonMutatingUseContext::Copy),
149+
loc,
150+
)
151+
}
152+
}
153+
154+
fn visit_operand(&mut self, operand: &mut Operand<'tcx>, loc: Location) {
155+
if let Operand::Move(place) = *operand
156+
&& let Some(local) = place.as_local()
157+
&& !self.fully_moved.contains(local)
158+
{
159+
*operand = Operand::Copy(place);
160+
}
161+
self.super_operand(operand, loc);
162+
}
163+
164+
fn visit_statement(&mut self, stmt: &mut Statement<'tcx>, loc: Location) {
165+
if let StatementKind::StorageDead(l) = stmt.kind
166+
&& self.storage_to_remove.contains(l)
167+
{
168+
stmt.make_nop();
169+
} else if let StatementKind::Assign(box (ref place, ref mut rvalue)) = stmt.kind
170+
&& place.as_local().is_some()
171+
{
172+
// Do not replace assignments.
173+
self.visit_rvalue(rvalue, loc)
174+
} else {
175+
self.super_statement(stmt, loc);
176+
}
177+
}
178+
}

compiler/rustc_mir_transform/src/dest_prop.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,8 @@ impl<'a, 'tcx> MutVisitor<'tcx> for Merger<'a, 'tcx> {
328328
match &statement.kind {
329329
StatementKind::Assign(box (dest, rvalue)) => {
330330
match rvalue {
331-
Rvalue::Use(Operand::Copy(place) | Operand::Move(place)) => {
331+
Rvalue::CopyForDeref(place)
332+
| Rvalue::Use(Operand::Copy(place) | Operand::Move(place)) => {
332333
// These might've been turned into self-assignments by the replacement
333334
// (this includes the original statement we wanted to eliminate).
334335
if dest == place {
@@ -755,7 +756,7 @@ impl<'tcx> Visitor<'tcx> for FindAssignments<'_, '_, 'tcx> {
755756
fn visit_statement(&mut self, statement: &Statement<'tcx>, _: Location) {
756757
if let StatementKind::Assign(box (
757758
lhs,
758-
Rvalue::Use(Operand::Copy(rhs) | Operand::Move(rhs)),
759+
Rvalue::CopyForDeref(rhs) | Rvalue::Use(Operand::Copy(rhs) | Operand::Move(rhs)),
759760
)) = &statement.kind
760761
{
761762
let Some((src, dest)) = places_to_candidate_pair(*lhs, *rhs, self.body) else {

compiler/rustc_mir_transform/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ mod const_debuginfo;
5454
mod const_goto;
5555
mod const_prop;
5656
mod const_prop_lint;
57+
mod copy_prop;
5758
mod coverage;
5859
mod ctfe_limit;
5960
mod dataflow_const_prop;
@@ -87,6 +88,7 @@ mod required_consts;
8788
mod reveal_all;
8889
mod separate_const_switch;
8990
mod shim;
91+
mod ssa;
9092
// This pass is public to allow external drivers to perform MIR cleanup
9193
pub mod simplify;
9294
mod simplify_branches;
@@ -563,6 +565,7 @@ fn run_optimization_passes<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
563565
&instcombine::InstCombine,
564566
&separate_const_switch::SeparateConstSwitch,
565567
&simplify::SimplifyLocals::new("before-const-prop"),
568+
&copy_prop::CopyProp,
566569
//
567570
// FIXME(#70073): This pass is responsible for both optimization as well as some lints.
568571
&const_prop::ConstProp,

compiler/rustc_mir_transform/src/simplify.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,18 @@ impl<'tcx> MirPass<'tcx> for SimplifyLocals {
404404
}
405405
}
406406

407+
pub fn remove_unused_definitions<'tcx>(body: &mut Body<'tcx>) {
408+
// First, we're going to get a count of *actual* uses for every `Local`.
409+
let mut used_locals = UsedLocals::new(body);
410+
411+
// Next, we're going to remove any `Local` with zero actual uses. When we remove those
412+
// `Locals`, we're also going to subtract any uses of other `Locals` from the `used_locals`
413+
// count. For example, if we removed `_2 = discriminant(_1)`, then we'll subtract one from
414+
// `use_counts[_1]`. That in turn might make `_1` unused, so we loop until we hit a
415+
// fixedpoint where there are no more unused locals.
416+
remove_unused_definitions_helper(&mut used_locals, body);
417+
}
418+
407419
pub fn simplify_locals<'tcx>(body: &mut Body<'tcx>, tcx: TyCtxt<'tcx>) {
408420
// First, we're going to get a count of *actual* uses for every `Local`.
409421
let mut used_locals = UsedLocals::new(body);
@@ -413,7 +425,7 @@ pub fn simplify_locals<'tcx>(body: &mut Body<'tcx>, tcx: TyCtxt<'tcx>) {
413425
// count. For example, if we removed `_2 = discriminant(_1)`, then we'll subtract one from
414426
// `use_counts[_1]`. That in turn might make `_1` unused, so we loop until we hit a
415427
// fixedpoint where there are no more unused locals.
416-
remove_unused_definitions(&mut used_locals, body);
428+
remove_unused_definitions_helper(&mut used_locals, body);
417429

418430
// Finally, we'll actually do the work of shrinking `body.local_decls` and remapping the `Local`s.
419431
let map = make_local_map(&mut body.local_decls, &used_locals);
@@ -548,7 +560,7 @@ impl<'tcx> Visitor<'tcx> for UsedLocals {
548560
}
549561

550562
/// Removes unused definitions. Updates the used locals to reflect the changes made.
551-
fn remove_unused_definitions(used_locals: &mut UsedLocals, body: &mut Body<'_>) {
563+
fn remove_unused_definitions_helper(used_locals: &mut UsedLocals, body: &mut Body<'_>) {
552564
// The use counts are updated as we remove the statements. A local might become unused
553565
// during the retain operation, leading to a temporary inconsistency (storage statements or
554566
// definitions referencing the local might remain). For correctness it is crucial that this

0 commit comments

Comments
 (0)