Skip to content

Remove fewer Storage calls in CopyProp and GVN #142531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d9829c1
Added a mir-opt test for generating storage statements for scoped locals
ohadravid Jun 3, 2025
814fe41
Implement `MaybeUninitializedLocals` analysis for `copy_prop` mir-opt…
ohadravid Jun 3, 2025
d65a0ce
Added gvn test for removed local storage annotations
ohadravid Jun 3, 2025
2ee4a9b
Use `MaybeUninitializedLocals` analysis in GVN mir-opt to remove fewe…
ohadravid Jun 14, 2025
f4ecbcf
Use a single bitset to check the storage in copy_prop, only check rea…
ohadravid Jun 29, 2025
bde405a
Added test for preserving head storage
ohadravid Jun 29, 2025
5063f8a
Move `MaybeUninitializedLocals` to the ssa module
ohadravid Jun 29, 2025
2c521dd
Added FileCheck to copy prop storage tests
ohadravid Jun 29, 2025
ce2e9b6
Simplify GVN storage checker to use a single bitset
ohadravid Jun 29, 2025
80fce82
Improve GVN storage tests
ohadravid Jun 29, 2025
9218a47
GVN unnit analysis shouldn't check unreachable blocks
ohadravid Jun 29, 2025
e2349a7
Add more FileCheck statements in gvn storage tests
ohadravid Jun 29, 2025
f85c6a4
Added a GVN storage test for borrowed value
ohadravid Jun 29, 2025
19679d3
Improved wording in comments and logs
ohadravid Jun 29, 2025
5e870b2
Added a storage usage inverse order test
ohadravid Jul 4, 2025
f9c282d
Move borrowed_locals filtering out of StorageChecker
ohadravid Jul 4, 2025
4f356a3
in copy_prop, we should only ignore storage statement
ohadravid Jul 4, 2025
675c79d
Remove `MaybeUninitializedLocals`'s new
ohadravid Jul 4, 2025
20134ed
Added another small test to gvn storage
ohadravid Jul 4, 2025
a543486
fix comment in copy_prop_storage_preserve_head.rs
ohadravid Jul 4, 2025
3404964
rerun and bless again after rebase (reordered storage statements)
ohadravid Jul 4, 2025
afe3d46
Update the tests after the change to copy_prop in #143509
ohadravid Jul 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions compiler/rustc_mir_transform/src/copy_prop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ use rustc_index::bit_set::DenseBitSet;
use rustc_middle::mir::visit::*;
use rustc_middle::mir::*;
use rustc_middle::ty::TyCtxt;
use rustc_mir_dataflow::{Analysis, ResultsCursor};
use tracing::{debug, instrument};

use crate::ssa::SsaLocals;
use crate::ssa::{MaybeUninitializedLocals, SsaLocals};

/// Unify locals that copy each other.
///
Expand All @@ -16,7 +17,7 @@ use crate::ssa::SsaLocals;
/// _d = move? _c
/// where each of the locals is only assigned once.
///
/// We want to replace all those locals by `_a`, either copied or moved.
/// We want to replace all those locals by `_a` (the "head"), either copied or moved.
pub(super) struct CopyProp;

impl<'tcx> crate::MirPass<'tcx> for CopyProp {
Expand All @@ -30,11 +31,13 @@ impl<'tcx> crate::MirPass<'tcx> for CopyProp {

let typing_env = body.typing_env(tcx);
let ssa = SsaLocals::new(tcx, body, typing_env);

debug!(borrowed_locals = ?ssa.borrowed_locals());
debug!(copy_classes = ?ssa.copy_classes());

let mut any_replacement = false;
let mut storage_to_remove = DenseBitSet::new_empty(body.local_decls.len());

for (local, &head) in ssa.copy_classes().iter_enumerated() {
if local != head {
any_replacement = true;
Expand All @@ -49,6 +52,44 @@ impl<'tcx> crate::MirPass<'tcx> for CopyProp {
let fully_moved = fully_moved_locals(&ssa, body);
debug!(?fully_moved);

// When emitting storage statements, we want to retain the head locals' storage statements,
// as this enables better optimizations. For each local use location, we mark the head for storage removal
// only if the head might be uninitialized at that point, or if the local is borrowed
// (since we cannot easily determine when it's used).
let storage_to_remove = if tcx.sess.emit_lifetime_markers() {
storage_to_remove.clear();

// If the local is borrowed, we cannot easily determine if it is used, so we have to remove the storage statements.
let borrowed_locals = ssa.borrowed_locals();

for (local, &head) in ssa.copy_classes().iter_enumerated() {
if local != head && borrowed_locals.contains(local) {
storage_to_remove.insert(head);
}
}

let maybe_uninit = MaybeUninitializedLocals
.iterate_to_fixpoint(tcx, body, Some("mir_opt::copy_prop"))
.into_results_cursor(body);

let mut storage_checker = StorageChecker {
maybe_uninit,
copy_classes: ssa.copy_classes(),
storage_to_remove,
};

for (bb, data) in traversal::reachable(body) {
storage_checker.visit_basic_block_data(bb, data);
}

storage_checker.storage_to_remove
} else {
// Remove the storage statements of all the head locals.
storage_to_remove
};

debug!(?storage_to_remove);

Replacer { tcx, copy_classes: ssa.copy_classes(), fully_moved, storage_to_remove }
.visit_body_preserves_cfg(body);

Expand Down Expand Up @@ -154,3 +195,39 @@ impl<'tcx> MutVisitor<'tcx> for Replacer<'_, 'tcx> {
}
}
}

// Marks heads of copy classes that are maybe uninitialized at the location of a local
// as needing storage statement removal.
struct StorageChecker<'a, 'tcx> {
maybe_uninit: ResultsCursor<'a, 'tcx, MaybeUninitializedLocals>,
copy_classes: &'a IndexSlice<Local, Local>,
storage_to_remove: DenseBitSet<Local>,
}

impl<'a, 'tcx> Visitor<'tcx> for StorageChecker<'a, 'tcx> {
fn visit_local(&mut self, local: Local, context: PlaceContext, loc: Location) {
if !context.is_use() {
return;
}

let head = self.copy_classes[local];

// If the local is the head, or if we already marked it for deletion, we do not need to check it.
if head == local || self.storage_to_remove.contains(head) {
return;
}

self.maybe_uninit.seek_before_primary_effect(loc);

if self.maybe_uninit.get().contains(head) {
debug!(
?loc,
?context,
?local,
?head,
"local's head is maybe uninit at this location, marking head for storage statement removal"
);
self.storage_to_remove.insert(head);
}
}
}
77 changes: 71 additions & 6 deletions compiler/rustc_mir_transform/src/gvn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,13 @@ use rustc_middle::mir::visit::*;
use rustc_middle::mir::*;
use rustc_middle::ty::layout::HasTypingEnv;
use rustc_middle::ty::{self, Ty, TyCtxt};
use rustc_mir_dataflow::{Analysis, ResultsCursor};
use rustc_span::DUMMY_SP;
use rustc_span::def_id::DefId;
use smallvec::SmallVec;
use tracing::{debug, instrument, trace};

use crate::ssa::SsaLocals;
use crate::ssa::{MaybeUninitializedLocals, SsaLocals};

pub(super) struct GVN;

Expand Down Expand Up @@ -140,10 +141,34 @@ impl<'tcx> crate::MirPass<'tcx> for GVN {
state.visit_basic_block_data(bb, data);
}

// For each local that is reused (`y` above), we remove its storage statements do avoid any
// difficulty. Those locals are SSA, so should be easy to optimize by LLVM without storage
// statements.
StorageRemover { tcx, reused_locals: state.reused_locals }.visit_body_preserves_cfg(body);
// When emitting storage statements, we want to retain the reused locals' storage statements,
// as this enables better optimizations. For each local use location, we mark it for storage removal
// only if it might be uninitialized at that point.
let storage_to_remove = if tcx.sess.emit_lifetime_markers() {
let maybe_uninit = MaybeUninitializedLocals
.iterate_to_fixpoint(tcx, body, Some("mir_opt::gvn"))
.into_results_cursor(body);

let mut storage_checker = StorageChecker {
reused_locals: &state.reused_locals,
storage_to_remove: DenseBitSet::new_empty(body.local_decls.len()),
maybe_uninit,
};

for (bb, data) in traversal::reachable(body) {
storage_checker.visit_basic_block_data(bb, data);
}

storage_checker.storage_to_remove
} else {
// Remove the storage statements of all the reused locals.
state.reused_locals.clone()
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is very close to the one in copy_prop. Could you merge them into a single compute_storage_to_remove function in rustc_mir_transform::ssa? It would take a set of relevant locals (copy-prop : copy_classes[local] != local, GVN : reused_locals) and compute storage_to_remove.

Copy link
Contributor Author

@ohadravid ohadravid Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can, but I think such a function will need to also accept a map: Fn(Local) -> Local (|local| copy_class[local] in copy_prop) since in copy_prop we check the head and not the actual visit_local's local param. We could change the storage check to run after the replacement, but then we'll need to add a separate StorageRemover (similar to GVN).

Edit: Also, after implementing #142531 (comment), the storage check logic diverged even more between the two passes.


debug!(?storage_to_remove);

StorageRemover { tcx, reused_locals: state.reused_locals, storage_to_remove }
.visit_body_preserves_cfg(body);
}

fn is_required(&self) -> bool {
Expand Down Expand Up @@ -1824,6 +1849,7 @@ impl<'tcx> MutVisitor<'tcx> for VnState<'_, 'tcx> {
struct StorageRemover<'tcx> {
tcx: TyCtxt<'tcx>,
reused_locals: DenseBitSet<Local>,
storage_to_remove: DenseBitSet<Local>,
}

impl<'tcx> MutVisitor<'tcx> for StorageRemover<'tcx> {
Expand All @@ -1844,11 +1870,50 @@ impl<'tcx> MutVisitor<'tcx> for StorageRemover<'tcx> {
match stmt.kind {
// When removing storage statements, we need to remove both (#107511).
StatementKind::StorageLive(l) | StatementKind::StorageDead(l)
if self.reused_locals.contains(l) =>
if self.storage_to_remove.contains(l) =>
{
stmt.make_nop()
}
_ => self.super_statement(stmt, loc),
}
}
}

struct StorageChecker<'a, 'tcx> {
reused_locals: &'a DenseBitSet<Local>,
storage_to_remove: DenseBitSet<Local>,
maybe_uninit: ResultsCursor<'a, 'tcx, MaybeUninitializedLocals>,
}

impl<'a, 'tcx> Visitor<'tcx> for StorageChecker<'a, 'tcx> {
fn visit_local(&mut self, local: Local, context: PlaceContext, location: Location) {
match context {
// These mutating uses do not require the local to be initialized.
PlaceContext::MutatingUse(MutatingUseContext::AsmOutput)
| PlaceContext::MutatingUse(MutatingUseContext::Call)
| PlaceContext::MutatingUse(MutatingUseContext::Store)
| PlaceContext::MutatingUse(MutatingUseContext::Yield)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do not require the local to be initialized, but they do require it to have storage. Mixing the two notions makes me uneasy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can probably add a test that triggers this. Is the right solution to use two separate analysis checks (so for these we'll check the MaybeStorageDead)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, thinking about this some more: we know that the reused_locals have valid storage before their original assignments.

For any replacements (eg target of the Store) the local cannot be a reused local anyway.

What do you think?

| PlaceContext::NonUse(_) => {
return;
}
// Must check validity for other mutating usages and all non-mutating uses.
PlaceContext::MutatingUse(_) | PlaceContext::NonMutatingUse(_) => {}
}

// We only need to check reused locals which we haven't already removed storage for.
if !self.reused_locals.contains(local) || self.storage_to_remove.contains(local) {
return;
}

self.maybe_uninit.seek_before_primary_effect(location);

if self.maybe_uninit.get().contains(local) {
debug!(
?location,
?local,
"local is reused and is maybe uninit at this location, marking it for storage statement removal"
);
self.storage_to_remove.insert(local);
}
}
}
69 changes: 69 additions & 0 deletions compiler/rustc_mir_transform/src/ssa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use rustc_middle::middle::resolve_bound_vars::Set1;
use rustc_middle::mir::visit::*;
use rustc_middle::mir::*;
use rustc_middle::ty::{self, TyCtxt};
use rustc_mir_dataflow::Analysis;
use tracing::{debug, instrument, trace};

pub(super) struct SsaLocals {
Expand Down Expand Up @@ -405,3 +406,71 @@ impl StorageLiveLocals {
matches!(self.storage_live[local], Set1::One(_))
}
}

/// A dataflow analysis that tracks locals that are maybe uninitialized.
///
/// This is a simpler analysis than `MaybeUninitializedPlaces`, because it does not track
/// individual fields.
pub(crate) struct MaybeUninitializedLocals;

impl<'tcx> Analysis<'tcx> for MaybeUninitializedLocals {
type Domain = DenseBitSet<Local>;

const NAME: &'static str = "maybe_uninit_locals";

fn bottom_value(&self, body: &Body<'tcx>) -> Self::Domain {
// bottom = all locals are initialized.
DenseBitSet::new_empty(body.local_decls.len())
}

fn initialize_start_block(&self, body: &Body<'tcx>, state: &mut Self::Domain) {
// All locals start as uninitialized...
state.insert_all();
// ...except for arguments, which are definitely initialized.
for arg in body.args_iter() {
state.remove(arg);
}
}

fn apply_primary_statement_effect(
&mut self,
state: &mut Self::Domain,
statement: &Statement<'tcx>,
_location: Location,
) {
match statement.kind {
// An assignment makes a local initialized.
StatementKind::Assign(box (place, _)) => {
if let Some(local) = place.as_local() {
state.remove(local);
}
}
// Deinit makes the local uninitialized.
StatementKind::Deinit(box place) => {
// A deinit makes a local uninitialized.
if let Some(local) = place.as_local() {
state.insert(local);
}
}
// Storage{Live,Dead} makes a local uninitialized.
StatementKind::StorageLive(local) | StatementKind::StorageDead(local) => {
state.insert(local);
}
_ => {}
}
}

fn apply_call_return_effect(
&mut self,
state: &mut Self::Domain,
_block: BasicBlock,
return_places: CallReturnPlaces<'_, 'tcx>,
) {
// The return place of a call is initialized.
return_places.for_each(|place| {
if let Some(local) = place.as_local() {
state.remove(local);
}
});
}
}
20 changes: 10 additions & 10 deletions tests/mir-opt/const_debuginfo.main.SingleUseConsts.diff
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@
}

bb0: {
nop;
StorageLive(_1);
- _1 = const 1_u8;
nop;
- _2 = const 2_u8;
nop;
- _3 = const 3_u8;
+ nop;
StorageLive(_2);
- _2 = const 2_u8;
+ nop;
StorageLive(_3);
- _3 = const 3_u8;
+ nop;
StorageLive(_4);
StorageLive(_5);
Expand Down Expand Up @@ -95,7 +95,7 @@
- _12 = const Point {{ x: 32_u32, y: 32_u32 }};
+ nop;
StorageLive(_13);
nop;
StorageLive(_14);
- _14 = const 32_u32;
+ nop;
StorageLive(_15);
Expand All @@ -104,17 +104,17 @@
+ nop;
+ nop;
StorageDead(_15);
nop;
StorageDead(_14);
_0 = const ();
StorageDead(_13);
StorageDead(_12);
StorageDead(_11);
StorageDead(_10);
StorageDead(_9);
StorageDead(_4);
nop;
nop;
nop;
StorageDead(_3);
StorageDead(_2);
StorageDead(_1);
return;
}
}
Expand Down
6 changes: 2 additions & 4 deletions tests/mir-opt/const_prop/aggregate.main.GVN.panic-abort.diff
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
}

bb0: {
- StorageLive(_1);
+ nop;
StorageLive(_1);
StorageLive(_2);
StorageLive(_3);
_3 = (const 0_i32, const 1_u8, const 2_i32);
Expand All @@ -36,8 +35,7 @@
StorageDead(_5);
StorageDead(_4);
_0 = const ();
- StorageDead(_1);
+ nop;
StorageDead(_1);
return;
}
}
Expand Down
Loading
Loading