Skip to content
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

Simplify switches on a statically known discriminant in MIR. #112688

Closed
wants to merge 1 commit into from
Closed

Simplify switches on a statically known discriminant in MIR. #112688

wants to merge 1 commit into from

Conversation

JohnBobbo96
Copy link
Contributor

@JohnBobbo96 JohnBobbo96 commented Jun 16, 2023

This PR adds a new MIR pass: SimplifyStaticSwitch, which aims to simplify switches in MIR that match on a known discriminant, this mainly happens as a result of MIR inlining.

Addresses: #111442 (comment)

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jun 16, 2023
@rustbot
Copy link
Collaborator

rustbot commented Jun 16, 2023

Some changes occurred to MIR optimizations

cc @rust-lang/wg-mir-opt

@bors
Copy link
Contributor

bors commented Jun 16, 2023

☔ The latest upstream changes (presumably #112346) made this pull request unmergeable. Please resolve the merge conflicts.

@scottmcm
Copy link
Member

cc @cjgillot specifically, to comment how this should go along with #107009

Copy link
Contributor

@cjgillot cjgillot left a comment

Choose a reason for hiding this comment

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

We already have SeparateConstSwitch and ConstProp, which taken together do a similar transformation. What is the gain of this pass compared to the current state?

}

fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
debug!(body = %tcx.def_path_str(body.source.def_id()));
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
debug!(body = %tcx.def_path_str(body.source.def_id()));
debug!(body = ?body.source.def_id());

def_path_str dooms the current compilation session.

#[instrument(level = "debug", skip(tcx, body), ret)]
fn simplify_static_switch<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> bool {
let mut new_targets = FxHashMap::default();
let predecessors: &IndexSlice<_, _> = body.basic_blocks.predecessors();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let predecessors: &IndexSlice<_, _> = body.basic_blocks.predecessors();
let predecessors = body.basic_blocks.predecessors();

continue;
}

let predecessors: &[BasicBlock] = &predecessors[block];
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let predecessors: &[BasicBlock] = &predecessors[block];
let predecessors = &predecessors[block];

fn simplify_static_switch<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> bool {
let mut new_targets = FxHashMap::default();
let predecessors: &IndexSlice<_, _> = body.basic_blocks.predecessors();
for (block, data) in traversal::preorder(body) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why this particular order?

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 thought it might be better than iterating normally but i have switched it back to just iter_enumerated.

Comment on lines 101 to 102
"{pred:?}: {place:?} = {}::{variant:?}; goto -> {block:?};",
tcx.def_path_str(def_id)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"{pred:?}: {place:?} = {}::{variant:?}; goto -> {block:?};",
tcx.def_path_str(def_id)
"{pred:?}: {place:?} = {def_id:?}::{variant:?}; goto -> {block:?};"

"{pred:?}: {place:?} = {}::{variant:?}; goto -> {block:?};",
tcx.def_path_str(def_id)
);
let discr_ty = tcx.type_of(def_id).skip_binder();
Copy link
Contributor

Choose a reason for hiding this comment

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

You must take the substitutions into account here, otherwise you'll get the wrong type.

@scottmcm scottmcm assigned cjgillot and unassigned scottmcm Jun 17, 2023
@JohnBobbo96
Copy link
Contributor Author

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 17, 2023
@JohnBobbo96
Copy link
Contributor Author

JohnBobbo96 commented Jun 19, 2023

We already have SeparateConstSwitch and ConstProp, which taken together do a similar transformation. What is the gain of this pass compared to the current state?

I'd say the main advantage is that this pass sort of combines the effect of SeparateConstSwitch + ConstProp except it looks for aggregates instead of constants, which currently doesn't seem to be handled by SeparateConstSwitch + ConstProp alone, see this godbolt link (the blocks that switch on the discriminant of _2 are still there, which this pass removes).

@JohnBobbo96
Copy link
Contributor Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jun 19, 2023
@@ -130,7 +131,7 @@ pub fn provide(providers: &mut Providers) {
mir_generator_witnesses: generator::mir_generator_witnesses,
optimized_mir,
is_mir_available,
is_ctfe_mir_available: |tcx, did| is_mir_available(tcx, did),
is_ctfe_mir_available: is_mir_available,
Copy link
Contributor

Choose a reason for hiding this comment

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

This change is unrelated. Could you submit it as a separate PR?

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 removed this for now, i'll open a PR soon.

@@ -532,7 +533,7 @@ fn run_runtime_cleanup_passes<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
}

fn run_optimization_passes<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
fn o1<T>(x: T) -> WithMinOptLevel<T> {
const fn o1<T>(x: T) -> WithMinOptLevel<T> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this useful?

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 also removed this.

Comment on lines 40 to 43
fn name(&self) -> &'static str {
"SimplifyStaticSwitch"
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
fn name(&self) -> &'static str {
"SimplifyStaticSwitch"
}

This is the default.

compiler/rustc_mir_transform/src/simplify_static_switch.rs Outdated Show resolved Hide resolved
}

let basic_blocks = body.basic_blocks.as_mut();
for ((place, block), new_targets) in new_targets {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is what you call discr earlier, could you name it so here?

vec![]
};

for (block, target) in new_targets {
Copy link
Contributor

Choose a reason for hiding this comment

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

block here is pred, please call it so.


for (block, target) in new_targets {
let data = &mut basic_blocks[block];
let old_goto = &mut data.terminator.as_mut().unwrap().kind;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let old_goto = &mut data.terminator.as_mut().unwrap().kind;
let old_goto = &mut data.terminator_mut().kind;

@@ -548,6 +549,8 @@ fn run_optimization_passes<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
&uninhabited_enum_branching::UninhabitedEnumBranching,
&o1(simplify::SimplifyCfg::AfterUninhabitedEnumBranching),
&inline::Inline,
// Remove switches on a statically known discriminant, which can happen as a result of inlining.
&simplify_static_switch::SimplifyStaticSwitch,
Copy link
Contributor

Choose a reason for hiding this comment

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

This pass clones statements. As we have SSA-based passes, and we don't try to recover SSAness, it should be run much later.

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 have moved the pass to right before SeparateConstSwitch.

Comment on lines 89 to 91
if place.as_local() == Some(switched)
&& let Some(local) = discr.as_local()
&& !borrowed_locals.contains(local) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Those checks should happen outside of the find_map, in cases there are several assignments to switched.

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 also moved these checks to outside of the find_map.

@JohnBobbo96
Copy link
Contributor Author

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 20, 2023
@JohnBobbo96
Copy link
Contributor Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jun 20, 2023
Copy link
Contributor

@cjgillot cjgillot left a comment

Choose a reason for hiding this comment

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

Thanks! This is starting to look good.
Could you add comments explaining the pattern you are looking for in this opt, and why it is sound?

// We need to make sure that `switched` is not overwritten before the switch,
// and also that `discr` is not overwritten before the `Rvalue::Discriminant`.
if is_local_mutated(body, block, discr, index, 0..index)
|| is_local_mutated(body, block, switched, index, index..data.statements.len())
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: can we just pass a range to this function?

let predecessors = body.basic_blocks.predecessors();
let borrowed_locals = borrowed_locals(body);
// Should this be an `IndexVec` instead?
let mut new_targets = FxHashMap::default();
Copy link
Contributor

Choose a reason for hiding this comment

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

A simple Vec should be enough, as you don't use it for indexing.

StatementKind::Assign(box (
place,
Rvalue::Aggregate(box AggregateKind::Adt(def_id, variant, substs, ..), ..),
)) if place.as_local() == Some(discr) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

You need to check if discr is mutated between this assignment and the end of block.

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 have added a check for this.

};

for statement in data.statements.iter().rev() {
match statement.kind {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: this can be an if-let.

for (block, new_targets) in new_targets {
let statements: Vec<_> = basic_blocks[block].statements.iter().cloned().collect();
for (pred, target) in new_targets {
let pred = &mut basic_blocks[pred];
Copy link
Contributor

Choose a reason for hiding this comment

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

Please document why there is only a single pred -> block edge in the whole CFG, and that no earlier threading has overwritten it.

Copy link
Contributor

Choose a reason for hiding this comment

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

It may also be useful to document why pred != block and assert it.

let statements: Vec<_> = basic_blocks[block].statements.iter().cloned().collect();
for (pred, target) in new_targets {
let pred = &mut basic_blocks[pred];
let old_goto = &mut pred.terminator_mut().kind;
Copy link
Contributor

Choose a reason for hiding this comment

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

We should assert that old_goto is indeed goto block, to ensure we have not overwritten it in the mean time.

Copy link
Contributor Author

@JohnBobbo96 JohnBobbo96 Jun 27, 2023

Choose a reason for hiding this comment

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

I have added an assert to check this, as well as not allowing two StaticSwitchs to have the same pred, so the terminator should not get overwritten.


let basic_blocks = body.basic_blocks.as_mut();
for (block, new_targets) in new_targets {
let statements: Vec<_> = basic_blocks[block].statements.iter().cloned().collect();
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we use IndexSlice::pick2_mut to avoid having to clone statements twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is cool, i also added this.

@@ -0,0 +1,29 @@
// unit-test: SimplifyStaticSwitch
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add more tests?
With all the extra checks I made you add + weird cfgs if possible.
See #107009 for a few examples.

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 have added a few more tests, including checks for mutation/borrowing.


// Something to be noted is that, this creates an edge from `pred -> target`
// which will only appear once in the CFG.
*old_goto = new_goto;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we prevent threading through a loop header, in order to avoid creating irreducible CFGs?

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 also added a check for this.

@JohnBobbo96
Copy link
Contributor Author

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 24, 2023
@JohnBobbo96
Copy link
Contributor Author

JohnBobbo96 commented Jun 27, 2023

Thanks! This is starting to look good. Could you add comments explaining the pattern you are looking for in this opt, and why it is sound?

Thank you for reviewing this!
I have also added more to the documentation explaining why i believe it is sound.

@JohnBobbo96
Copy link
Contributor Author

@rustbot ready

@rustbot rustbot removed the S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. label Jun 27, 2023
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jun 27, 2023
@rust-log-analyzer

This comment has been minimized.

removes unnecessary switches on statically known
discriminants.
Comment on lines +106 to +109
.statements
.iter()
.enumerate()
.take_while(|&(index, _)| index != statement_index)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.statements
.iter()
.enumerate()
.take_while(|&(index, _)| index != statement_index)
.statements[..statement_index]
.iter()
.enumerate()

continue
};

if place.local != switched {
Copy link
Contributor

Choose a reason for hiding this comment

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

local has necessarily an integer type. Should we assert that place.projection is empty?

continue;
}

let mut finder = MutatedLocalFinder { local: discr, mutated: false };
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be created inside the loop, to ensure we don't forget to reset mutated.

| StatementKind::Assign(box (
place,
Rvalue::Aggregate(box AggregateKind::Adt(_, variant, ..), ..),
)) if place.local == discr => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Assert place.projection is empty here too.


continue 'preds;
}
_ => finder.visit_statement(statement, Location { block, statement_index }),
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we create finder, visit, and check mutated in this arm, instead of the other arms?

Some(exclude) => {
pred.statements.extend(block.statements.iter().enumerate().filter_map(
|(index, statement)| {
if index == exclude { None } else { Some(statement.clone()) }
Copy link
Contributor

Choose a reason for hiding this comment

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

That's not necessary. We can run remove_unused_definitions together with remove_dead_blocks.

fn visit_place(&mut self, place: &Place<'tcx>, context: PlaceContext, _: Location) {
if self.local == place.local && let PlaceContext::MutatingUse(..) = context {
self.mutated = true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we detect storage statements too?

@@ -51,51 +51,44 @@
StorageLive(_10);
StorageLive(_11);
_9 = discriminant(_1);
switchInt(move _9) -> [0: bb7, 1: bb5, otherwise: bb6];
switchInt(move _9) -> [0: bb5, 1: bb3, otherwise: bb4];
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we make this a unit test for SeparateConstSwitch?


// We use the SSA, to destroy the SSA.
let data = {
let (block, pred) = basic_blocks.pick2_mut(block, switch.pred);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add a comment explaining why block != switch.pred?

}

let basic_blocks = body.basic_blocks.as_mut();
let num_switches: usize = static_switches.iter().map(|(_, switches)| switches.len()).sum();
Copy link
Contributor

Choose a reason for hiding this comment

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

Counting the number of opts just for tracing is not useful.

@cjgillot cjgillot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 4, 2023
@bors
Copy link
Contributor

bors commented Jul 14, 2023

☔ The latest upstream changes (presumably #109025) made this pull request unmergeable. Please resolve the merge conflicts.

@JohnCSimon
Copy link
Member

@JohnBobbo96

ping from triage - can you post your status on this PR? There hasn't been an update in a few months and there are merge conflicts Thanks!

FYI: when a PR is ready for review, send a message containing
@rustbot ready to switch to S-waiting-on-review so the PR is in the reviewer's backlog.

@JohnCSimon
Copy link
Member

@JohnBobbo96

Ping from triage: I'm closing this due to inactivity, Please reopen when you are ready to continue with this.
Note: if you are going to continue please open the PR BEFORE you push to it, else you won't be able to reopen - this is a quirk of github.
Thanks for your contribution.

@rustbot label: +S-inactive

@JohnCSimon JohnCSimon closed this Dec 17, 2023
@rustbot rustbot added the S-inactive Status: Inactive and waiting on the author. This is often applied to closed PRs. label Dec 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-inactive Status: Inactive and waiting on the author. This is often applied to closed PRs. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants