rust/compiler/rustc_mir_transform/src/separate_const_switch.rs
2021-09-25 01:08:41 +01:00

346 lines
14 KiB
Rust

//! A pass that duplicates switch-terminated blocks
//! into a new copy for each predecessor, provided
//! the predecessor sets the value being switched
//! over to a constant.
//!
//! The purpose of this pass is to help constant
//! propagation passes to simplify the switch terminator
//! of the copied blocks into gotos when some predecessors
//! statically determine the output of switches.
//!
//! ```text
//! x = 12 --- ---> something
//! \ / 12
//! --> switch x
//! / \ otherwise
//! x = y --- ---> something else
//! ```
//! becomes
//! ```text
//! x = 12 ---> switch x ------> something
//! \ / 12
//! X
//! / \ otherwise
//! x = y ---> switch x ------> something else
//! ```
//! so it can hopefully later be turned by another pass into
//! ```text
//! x = 12 --------------------> something
//! / 12
//! /
//! / otherwise
//! x = y ---- switch x ------> something else
//! ```
//!
//! This optimization is meant to cover simple cases
//! like `?` desugaring. For now, it thus focuses on
//! simplicity rather than completeness (it notably
//! sometimes duplicates abusively).
use crate::MirPass;
use rustc_middle::mir::*;
use rustc_middle::ty::TyCtxt;
use smallvec::SmallVec;
pub struct SeparateConstSwitch;
impl<'tcx> MirPass<'tcx> for SeparateConstSwitch {
fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
if tcx.sess.mir_opt_level() < 4 {
return;
}
// If execution did something, applying a simplification layer
// helps later passes optimize the copy away.
if separate_const_switch(body) > 0 {
super::simplify::simplify_cfg(tcx, body);
}
}
}
/// Returns the amount of blocks that were duplicated
pub fn separate_const_switch<'tcx>(body: &mut Body<'tcx>) -> usize {
let mut new_blocks: SmallVec<[(BasicBlock, BasicBlock); 6]> = SmallVec::new();
let predecessors = body.predecessors();
'block_iter: for (block_id, block) in body.basic_blocks().iter_enumerated() {
if let TerminatorKind::SwitchInt {
discr: Operand::Copy(switch_place) | Operand::Move(switch_place),
..
} = block.terminator().kind
{
// If the block is on an unwind path, do not
// apply the optimization as unwind paths
// rely on a unique parent invariant
if block.is_cleanup {
continue 'block_iter;
}
// If the block has fewer than 2 predecessors, ignore it
// we could maybe chain blocks that have exactly one
// predecessor, but for now we ignore that
if predecessors[block_id].len() < 2 {
continue 'block_iter;
}
// First, let's find a non-const place
// that determines the result of the switch
if let Some(switch_place) = find_determining_place(switch_place, block) {
// We now have an input place for which it would
// be interesting if predecessors assigned it from a const
let mut predecessors_left = predecessors[block_id].len();
'predec_iter: for predecessor_id in predecessors[block_id].iter().copied() {
let predecessor = &body.basic_blocks()[predecessor_id];
// First we make sure the predecessor jumps
// in a reasonable way
match &predecessor.terminator().kind {
// The following terminators are
// unconditionally valid
TerminatorKind::Goto { .. } | TerminatorKind::SwitchInt { .. } => {}
TerminatorKind::FalseEdge { real_target, .. } => {
if *real_target != block_id {
continue 'predec_iter;
}
}
// The following terminators are not allowed
TerminatorKind::Resume
| TerminatorKind::Drop { .. }
| TerminatorKind::DropAndReplace { .. }
| TerminatorKind::Call { .. }
| TerminatorKind::Assert { .. }
| TerminatorKind::FalseUnwind { .. }
| TerminatorKind::Yield { .. }
| TerminatorKind::Abort
| TerminatorKind::Return
| TerminatorKind::Unreachable
| TerminatorKind::InlineAsm { .. }
| TerminatorKind::GeneratorDrop => {
continue 'predec_iter;
}
}
if is_likely_const(switch_place, predecessor) {
new_blocks.push((predecessor_id, block_id));
predecessors_left -= 1;
if predecessors_left < 2 {
// If the original block only has one predecessor left,
// we have nothing left to do
break 'predec_iter;
}
}
}
}
}
}
// Once the analysis is done, perform the duplication
let body_span = body.span;
let copied_blocks = new_blocks.len();
let blocks = body.basic_blocks_mut();
for (pred_id, target_id) in new_blocks {
let new_block = blocks[target_id].clone();
let new_block_id = blocks.push(new_block);
let terminator = blocks[pred_id].terminator_mut();
match terminator.kind {
TerminatorKind::Goto { ref mut target } => {
*target = new_block_id;
}
TerminatorKind::FalseEdge { ref mut real_target, .. } => {
if *real_target == target_id {
*real_target = new_block_id;
}
}
TerminatorKind::SwitchInt { ref mut targets, .. } => {
targets.all_targets_mut().iter_mut().for_each(|x| {
if *x == target_id {
*x = new_block_id;
}
});
}
TerminatorKind::Resume
| TerminatorKind::Abort
| TerminatorKind::Return
| TerminatorKind::Unreachable
| TerminatorKind::GeneratorDrop
| TerminatorKind::Assert { .. }
| TerminatorKind::DropAndReplace { .. }
| TerminatorKind::FalseUnwind { .. }
| TerminatorKind::Drop { .. }
| TerminatorKind::Call { .. }
| TerminatorKind::InlineAsm { .. }
| TerminatorKind::Yield { .. } => {
span_bug!(
body_span,
"basic block terminator had unexpected kind {:?}",
&terminator.kind
)
}
}
}
copied_blocks
}
/// This function describes a rough heuristic guessing
/// whether a place is last set with a const within the block.
/// Notably, it will be overly pessimistic in cases that are already
/// not handled by `separate_const_switch`.
fn is_likely_const<'tcx>(mut tracked_place: Place<'tcx>, block: &BasicBlockData<'tcx>) -> bool {
for statement in block.statements.iter().rev() {
match &statement.kind {
StatementKind::Assign(assign) => {
if assign.0 == tracked_place {
match assign.1 {
// These rvalues are definitely constant
Rvalue::Use(Operand::Constant(_))
| Rvalue::Ref(_, _, _)
| Rvalue::AddressOf(_, _)
| Rvalue::Cast(_, Operand::Constant(_), _)
| Rvalue::NullaryOp(_, _)
| Rvalue::ShallowInitBox(_, _)
| Rvalue::UnaryOp(_, Operand::Constant(_)) => return true,
// These rvalues make things ambiguous
Rvalue::Repeat(_, _)
| Rvalue::ThreadLocalRef(_)
| Rvalue::Len(_)
| Rvalue::BinaryOp(_, _)
| Rvalue::CheckedBinaryOp(_, _)
| Rvalue::Aggregate(_, _) => return false,
// These rvalues move the place to track
Rvalue::Cast(_, Operand::Copy(place) | Operand::Move(place), _)
| Rvalue::Use(Operand::Copy(place) | Operand::Move(place))
| Rvalue::UnaryOp(_, Operand::Copy(place) | Operand::Move(place))
| Rvalue::Discriminant(place) => tracked_place = place,
}
}
}
// If the discriminant is set, it is always set
// as a constant, so the job is done.
// As we are **ignoring projections**, if the place
// we are tracking sees its discriminant be set,
// that means we had to be tracking the discriminant
// specifically (as it is impossible to switch over
// an enum directly, and if we were switching over
// its content, we would have had to at least cast it to
// some variant first)
StatementKind::SetDiscriminant { place, .. } => {
if **place == tracked_place {
return true;
}
}
// If inline assembly is found, we probably should
// not try to analyze the code
StatementKind::LlvmInlineAsm(_) => return false,
// These statements have no influence on the place
// we are interested in
StatementKind::FakeRead(_)
| StatementKind::StorageLive(_)
| StatementKind::Retag(_, _)
| StatementKind::AscribeUserType(_, _)
| StatementKind::Coverage(_)
| StatementKind::StorageDead(_)
| StatementKind::CopyNonOverlapping(_)
| StatementKind::Nop => {}
}
}
// If no good reason for the place to be const is found,
// give up. We could maybe go up predecessors, but in
// most cases giving up now should be sufficient.
false
}
/// Finds a unique place that entirely determines the value
/// of `switch_place`, if it exists. This is only a heuristic.
/// Ideally we would like to track multiple determining places
/// for some edge cases, but one is enough for a lot of situations.
fn find_determining_place<'tcx>(
mut switch_place: Place<'tcx>,
block: &BasicBlockData<'tcx>,
) -> Option<Place<'tcx>> {
for statement in block.statements.iter().rev() {
match &statement.kind {
StatementKind::Assign(op) => {
if op.0 != switch_place {
continue;
}
match op.1 {
// The following rvalues move the place
// that may be const in the predecessor
Rvalue::Use(Operand::Move(new) | Operand::Copy(new))
| Rvalue::UnaryOp(_, Operand::Copy(new) | Operand::Move(new))
| Rvalue::Cast(_, Operand::Move(new) | Operand::Copy(new), _)
| Rvalue::Repeat(Operand::Move(new) | Operand::Copy(new), _)
| Rvalue::Discriminant(new)
=> switch_place = new,
// The following rvalues might still make the block
// be valid but for now we reject them
Rvalue::Len(_)
| Rvalue::Ref(_, _, _)
| Rvalue::BinaryOp(_, _)
| Rvalue::CheckedBinaryOp(_, _)
| Rvalue::Aggregate(_, _)
// The following rvalues definitely mean we cannot
// or should not apply this optimization
| Rvalue::Use(Operand::Constant(_))
| Rvalue::Repeat(Operand::Constant(_), _)
| Rvalue::ThreadLocalRef(_)
| Rvalue::AddressOf(_, _)
| Rvalue::NullaryOp(_, _)
| Rvalue::ShallowInitBox(_, _)
| Rvalue::UnaryOp(_, Operand::Constant(_))
| Rvalue::Cast(_, Operand::Constant(_), _)
=> return None,
}
}
// These statements have no influence on the place
// we are interested in
StatementKind::FakeRead(_)
| StatementKind::StorageLive(_)
| StatementKind::StorageDead(_)
| StatementKind::Retag(_, _)
| StatementKind::AscribeUserType(_, _)
| StatementKind::Coverage(_)
| StatementKind::CopyNonOverlapping(_)
| StatementKind::Nop => {}
// If inline assembly is found, we probably should
// not try to analyze the code
StatementKind::LlvmInlineAsm(_) => return None,
// If the discriminant is set, it is always set
// as a constant, so the job is already done.
// As we are **ignoring projections**, if the place
// we are tracking sees its discriminant be set,
// that means we had to be tracking the discriminant
// specifically (as it is impossible to switch over
// an enum directly, and if we were switching over
// its content, we would have had to at least cast it to
// some variant first)
StatementKind::SetDiscriminant { place, .. } => {
if **place == switch_place {
return None;
}
}
}
}
Some(switch_place)
}