Implement RFC 3943: MIR move elimination#157943
Conversation
|
|
This comment has been minimized.
This comment has been minimized.
- Changed `iter_intervals` to return `RangeInclusive` instead of `Range` - Added `clear_row`, `disjoint_rows` and `intersects_range` methods
This flag also has the effect of disabling DestinationPropagation, which is already covered by move elimination.
f12157e to
ca2c2ac
Compare
| // | ||
| // This pass has 2 outputs: a set of kill points that mark the last use | ||
| // locations of locals and a per-block bitset indicating which locals are live | ||
| // on entry to that block. |
There was a problem hiding this comment.
Can we use a named struct for this?
| tcx: TyCtxt<'tcx>, | ||
| body: &mir::Body<'tcx>, | ||
| pass_name: Option<&'static str>, | ||
| ) -> (Vec<(Local, Location)>, IndexVec<BasicBlock, DenseBitSet<Local>>) { |
There was a problem hiding this comment.
BitMatrix<BasicBlock, Local>?
| // Notably this kills any dead results produced by a predecessor's terminator. | ||
| state.intersect(&live_on_entry[block]); | ||
|
|
||
| for local in state.iter() { | ||
| builder.gen_(local, points.entry_point(block), SplitPointEffect::Early); | ||
| } | ||
|
|
||
| for (statement_index, statement) in block_data.statements.iter().enumerate() { | ||
| let location = Location { block, statement_index }; | ||
| let point = points.point_from_location(location); | ||
|
|
||
| // StorageDead always kills a local, even if it has been borrowed. | ||
| if let mir::StatementKind::StorageDead(local) = statement.kind { | ||
| builder.kill(local, point, SplitPointEffect::Late); | ||
| continue; | ||
| } | ||
|
|
||
| // Kill moved operands if the whole local was moved. | ||
| VisitPlacesWith(|place: Place<'tcx>, ctxt| { | ||
| if ctxt == PlaceContext::NonMutatingUse(NonMutatingUseContext::Move) { | ||
| if let Some(local) = place.as_local() { | ||
| builder.kill(local, point, SplitPointEffect::Early); | ||
| } | ||
| } | ||
| }) | ||
| .visit_statement(statement, location); | ||
|
|
||
| // Kill any locals which are no longer used after this statement. | ||
| for &(local, _) in kill_point_map[point] { | ||
| builder.kill(local, point, SplitPointEffect::Early); | ||
| } | ||
|
|
||
| // Gen destination places. | ||
| VisitPlacesWith(|place: Place<'tcx>, ctxt| match DefUse::for_place(place, ctxt) { | ||
| DefUse::Def | DefUse::PartialWrite => { | ||
| builder.gen_(place.local, point, SplitPointEffect::Late) | ||
| } | ||
| DefUse::Use | DefUse::NonUse => {} | ||
| }) | ||
| .visit_statement(statement, location); | ||
|
|
||
| // Kill any dead destination places: they will only appear at | ||
| // the late point of the statement they are generated in, which is | ||
| // sufficient for determining overlap. | ||
| for &(local, _) in kill_point_map[point] { | ||
| builder.kill(local, point, SplitPointEffect::Late); | ||
| } | ||
| } | ||
|
|
||
| let location = Location { block, statement_index: block_data.statements.len() }; | ||
| let point = points.point_from_location(location); | ||
| let terminator = block_data.terminator(); | ||
|
|
||
| // Kill moved operands if the whole local was moved. Also kill dropped | ||
| // places if the entire local was dropped. | ||
| VisitPlacesWith(|place: Place<'tcx>, ctxt| { | ||
| if let PlaceContext::NonMutatingUse(NonMutatingUseContext::Move) | ||
| | PlaceContext::MutatingUse(MutatingUseContext::Drop) = ctxt | ||
| { | ||
| if let Some(local) = place.as_local() { | ||
| builder.kill(local, point, SplitPointEffect::Early); | ||
| } | ||
| } | ||
| }) | ||
| .visit_terminator(terminator, location); | ||
|
|
||
| // Kill any locals which are no longer used after this terminator. | ||
| for &(local, _) in kill_point_map[point] { | ||
| builder.kill(local, point, SplitPointEffect::Early); | ||
| } | ||
|
|
||
| // Gen destination places. | ||
| VisitPlacesWith(|place: Place<'tcx>, ctxt| match DefUse::for_place(place, ctxt) { | ||
| DefUse::Def | DefUse::PartialWrite => { | ||
| builder.gen_(place.local, point, SplitPointEffect::Late) | ||
| } | ||
| DefUse::Use | DefUse::NonUse => {} | ||
| }) | ||
| .visit_terminator(terminator, location); | ||
|
|
||
| // Move arguments to a call are treated specially: the place that they | ||
| // represent is passed directly to the callee, which means that they are | ||
| // not allowed to alias any other move operand or the destination place. | ||
| // This is represented here by extending their live range to the late | ||
| // part, making it overlap with that of the destination place. | ||
| // | ||
| // Notably, this *doesn't* apply to TailCall. | ||
| if let mir::TerminatorKind::Call { | ||
| func: _, | ||
| args, | ||
| destination: _, | ||
| target: _, | ||
| unwind: _, | ||
| call_source: _, | ||
| fn_span: _, | ||
| } = &terminator.kind | ||
| { | ||
| for arg in args { | ||
| if let mir::Operand::Move(place) = arg.node { | ||
| builder.gen_(place.local, point, SplitPointEffect::Late); | ||
| builder.kill(place.local, point, SplitPointEffect::Late); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This duplicates a lot of code in impl Analysis for PreciseLiveness. Any way to deduplicate?
| trace!("cannot unify {a:?} and {b:?} involving a rust-call tuple argument"); | ||
| return None; | ||
| } | ||
|
|
There was a problem hiding this comment.
Can we remove a common projection tail from a and b before this match? Allowing to merge _1.foo.bar with _2.blah.foo.bar?
| fn visit_aggregate_assign( | ||
| &mut self, | ||
| dest: Place<'tcx>, | ||
| project_field: impl Fn(TyCtxt<'tcx>, Place<'tcx>, FieldIdx, Ty<'tcx>) -> Place<'tcx>, |
There was a problem hiding this comment.
Should the callback return a &[PlaceElem<'tcx>] and call project_deeper here?
| let Some(next) = next else { | ||
| break; | ||
| }; | ||
| state.block = next; | ||
| } |
There was a problem hiding this comment.
IIUC, the loop and this special case is meant to avoid cloning the state.used_after. What about just calling stack.push(TailState { block: next, state: state.used_after }), and letting the while let loop handle everything?
| // Under the local lifetime semantics from RFC 3943, `StorageLive` does not allocate, | ||
| // and `StorageDead` has no effect if the local was already freed by a move. These | ||
| // markers therefore do not affect whether a copy can be treated as a final use. | ||
| StatementKind::StorageLive(_) | StatementKind::StorageDead(_) | StatementKind::Nop => {} |
There was a problem hiding this comment.
Why can't StorageDead(l) perform a used_after.remove(l)?
| if !process_rvalue(&mut assign.1, used_after) { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
| if !process_rvalue(&mut assign.1, used_after) { | |
| return false; | |
| } | |
| return process_rvalue(&mut assign.1, used_after); |
?
| block: BasicBlock, | ||
| used_after: &mut DenseBitSet<Local>, | ||
| borrowed: &DenseBitSet<Local>, | ||
| ) -> bool { |
There was a problem hiding this comment.
Would it be better to return a Result or ControlFlow here? Just a bool is a bit obscure.
| StatementKind::Assign(assign) => { | ||
| let dest = assign.0; |
There was a problem hiding this comment.
Could you untuple assign inside the match pattern?
This PR implements rust-lang/rfcs#3943: a MIR optimization pass that eliminates unnecessary copies. Since the new pass relies on the new MIR semantics from RFC 3943, it is gated behind the the
-Z mir-move-eliminationflag. Enabling this flag also disables the DestinationPropagation pass, which is completely superseded by this one.There are 3 main parts to this optimization:
The RFC text and the top-level comments for the various passes have more details on the internals of the optimization.
r? @dianqk
cc @rust-lang/wg-mir-opt