-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Conversation
Some changes occurred to MIR optimizations cc @rust-lang/wg-mir-opt |
☔ The latest upstream changes (presumably #112346) made this pull request unmergeable. Please resolve the merge conflicts. |
There was a problem hiding this 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())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let predecessors: &IndexSlice<_, _> = body.basic_blocks.predecessors(); | |
let predecessors = body.basic_blocks.predecessors(); |
continue; | ||
} | ||
|
||
let predecessors: &[BasicBlock] = &predecessors[block]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this particular order?
There was a problem hiding this comment.
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
.
"{pred:?}: {place:?} = {}::{variant:?}; goto -> {block:?};", | ||
tcx.def_path_str(def_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"{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(); |
There was a problem hiding this comment.
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.
@rustbot author |
I'd say the main advantage is that this pass sort of combines the effect of |
@rustbot ready |
@@ -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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this useful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i also removed this.
fn name(&self) -> &'static str { | ||
"SimplifyStaticSwitch" | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fn name(&self) -> &'static str { | |
"SimplifyStaticSwitch" | |
} |
This is the default.
} | ||
|
||
let basic_blocks = body.basic_blocks.as_mut(); | ||
for ((place, block), new_targets) in new_targets { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
if place.as_local() == Some(switched) | ||
&& let Some(local) = discr.as_local() | ||
&& !borrowed_locals.contains(local) => |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
.
@rustbot author |
@rustbot ready |
There was a problem hiding this 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()) |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 StaticSwitch
s 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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@rustbot author |
Thank you for reviewing this! |
@rustbot ready |
This comment has been minimized.
This comment has been minimized.
removes unnecessary switches on statically known discriminants.
.statements | ||
.iter() | ||
.enumerate() | ||
.take_while(|&(index, _)| index != statement_index) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.statements | |
.iter() | |
.enumerate() | |
.take_while(|&(index, _)| index != statement_index) | |
.statements[..statement_index] | |
.iter() | |
.enumerate() |
continue | ||
}; | ||
|
||
if place.local != switched { |
There was a problem hiding this comment.
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 }; |
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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 }), |
There was a problem hiding this comment.
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()) } |
There was a problem hiding this comment.
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; | ||
} |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
☔ The latest upstream changes (presumably #109025) made this pull request unmergeable. Please resolve the merge conflicts. |
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 |
Ping from triage: I'm closing this due to inactivity, Please reopen when you are ready to continue with this. @rustbot label: +S-inactive |
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)