Conversation
|
|
This comment has been minimized.
This comment has been minimized.
f12157e to
ca2c2ac
Compare
| 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>?
There was a problem hiding this comment.
In PreciseLiveness::apply_block_start_effect we need to have a DenseBitSet<Local> to call .intersect on, which BitMatrix doesn't expose.
| // 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?
There was a problem hiding this comment.
Not easily: the two are sufficiently different that I'm not sure the code would be clearer if we tried to deduplicate. The PreciseLiveness analysis is simpler since it only needs to determine the liveness at the end of the statement. On the other hand matrix construction needs to determine the liveness both at the mid point of the instruction and after it, and record both in the matrix.
| 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?
There was a problem hiding this comment.
We could, but that's not really useful in practice if you consider the kind of patterns that trigger this optimization. Specifically, in order for the lifetimes to not overlap the RHS of an assignment needs to end its lifetime at the assignment. This only happens for a move of a whole local (no projections) or for non-borrowed locals where this is its last use.
| 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?
There was a problem hiding this comment.
Returning a slice is awkward because there's no good lifetime to give it.
| // 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)?
There was a problem hiding this comment.
This is a backwards analysis where used_after tracks any local that is used later on the current path. We only ever add to used_after as we walk backwards through the function, and never remove from it.
This optimization is meant to supersede Separately, I think there may be value in having the coroutine pass re-use |
6724dd7 to
974d4c0
Compare
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
974d4c0 to
34776e8
Compare
This comment has been minimized.
This comment has been minimized.
34776e8 to
454a459
Compare
| VisitPlacesWith(|place: Place<'tcx>, ctxt| { | ||
| if let PlaceContext::NonMutatingUse(NonMutatingUseContext::Move) = ctxt { | ||
| if let Some(local) = place.as_local() { | ||
| builder.kill(local, point, SplitPointEffect::Early); |
There was a problem hiding this comment.
Question: Should we just treat the call arguments as live and ignore gen and kill for them? I don't see how that helps with move elimination, though I know it makes the analysis a bit less precise.
There was a problem hiding this comment.
Are you talking about argument locals (_1, _2) that come function arguments? We do want to track lifetimes for those because we can merge them with other locals if their lifetimes don't overlap.
There was a problem hiding this comment.
Yes. Here is an example: f98897f which skips gen&kill for arguments. I don't see tests that are broken. I cannot imagine that a case can use the move fact. Or can we only kill in the late phase?
There was a problem hiding this comment.
We still need to kill moved arguments after the call. It's just that they are killed at the late point instead of the early point to model the fact that they are not allowed to overlap with the return place.
There was a problem hiding this comment.
I think it's fine to skip kills, which is an over-approximation, still has the fact. IIUC, the current code is:
// Early: Kill(_1)
_2 = Call(move _1) -> BB1, BB2
// Late: Gen(_1, _2), Kill(_1)
I mean change this to:
// Early: -
_2 = Call(move _1) -> BB1, BB2
// Late: Gen(_2)
Or can we change to:
// Early: -
_2 = Call(move _1) -> BB1, BB2
// Late: Gen(_2), Kill(_1)
or
// Early: -
_2 = Call(move _1) -> BB1, BB2
// Late: Gen(_2)
BB1:
Kill(_1)
BB2:
Kill(_1)
?
There was a problem hiding this comment.
I understand what you mean now. You're right, the code looks better the way you suggest in f98897f. Since Call is a terminator, the kill_all at the end will end the lifetimes of any moved call operands at the late point anyways.
There was a problem hiding this comment.
Actually that still doesn't work because the kill point loop immediately afterwards may end the lifetime of a call argument early, which is incorrect if it is a move argument. So in the end my preference is for keeping the code as it is today.
|
I threw rustlantis at this PR and it found that this program: #![feature(custom_mir, core_intrinsics, lazy_get)]
extern crate core;
use core::intrinsics::mir::*;
fn dump_var<T>(val: T) { }
#[custom_mir(dialect = "runtime", phase = "initial")]
pub fn fn1() {
mir!{
let _1: *const bool;
let _2: bool;
let _3: bool;
{
_1 = core::ptr::addr_of!(_3);
*_1 = true;
_2 = _3;
Call(RET = dump_var(Move(_2)), ReturnTo(bb1), UnwindUnreachable())
}
bb1 = {
Call(RET = dump_var(_3), ReturnTo(bb2), UnwindUnreachable())
}
bb2 = {
Return()
}
}
}
pub fn main() {
fn1();
}gets this diff applied: -// MIR for `fn1` before MoveElimination
+// MIR for `fn1` after MoveElimination
fn fn1() -> () {
let mut _0: ();
@@ -7,14 +7,14 @@ fn fn1() -> () {
let mut _3: bool;
bb0: {
- _1 = &raw const _3;
+ _1 = &raw const _2;
(*_1) = const true;
- _2 = copy _3;
+ nop;
_0 = dump_var::<bool>(move _2) -> [return: bb1, unwind unreachable];
}
bb1: {
- _0 = dump_var::<bool>(copy _3) -> [return: bb2, unwind unreachable];
+ _0 = dump_var::<bool>(copy _2) -> [return: bb2, unwind unreachable];
}
bb2: {It seems like the current implementation introduces use of a place after it is moved out of? |
|
That's strange, it should have refused to merge _2 and _3 because their live ranges overlap. I'll look into it. |
|
|
|
The original program has no storage statement which means all locals are implicitly live at the beginning. |
|
Not with the new MIR semantics. The local is only allocated when it is first written to. |
|
Ah... then Rustlantis will need to be patched. |
This flag also has the effect of disabling DestinationPropagation, which is already covered by move elimination.
d70d9f0 to
b04dd7b
Compare
|
All feedback has been addressed:
@bors try @rust-timer queue |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Implement RFC 3943: MIR move elimination
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Finished benchmarking commit (205c95f): comparison URL. Overall result: ❌✅ regressions and improvements - please read:Benchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf. Next, please: If you can, justify the regressions found in this try perf run in writing along with @bors rollup=never rustc-perf Instruction countOur most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.
Max RSS (memory usage)Results (primary -0.9%, secondary -1.7%)A less reliable metric. May be of interest, but not used to determine the overall result above.
CyclesResults (primary -2.2%, secondary 2.6%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeResults (primary 0.6%, secondary 1.7%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Bootstrap: 475.357s -> 469.357s (-1.26%) |
842859f to
03564de
Compare
|
I opened #162048 for the Miri/rustc_const_eval adaptations needed for the new MIR semantics. It builds on top of this PR. |
|
☔ The latest upstream changes (presumably #160524) made this pull request unmergeable. Please resolve the merge conflicts by rebasing. |
|
Could you split this into smaller pull request? For example into |
I wouldn't split For the rest, I can make 4 PRs like this:
Let me know if that is OK, and who I should assign as a reviewer. |
|
Sounds good. You can assign me as a reviewer. I would suggest |
|
Sounds good too. I should be able to make time for the smaller PRs. Edited: I should be available during the first week of October. |
|
I've been working on the Miri support and it uncovered several issues with the semantics. So I'll be focusing on fixing those first. |
View all comments
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