Repository navigation
Add merge yields pass - #162429
Add merge yields pass#162429diondokter wants to merge 12 commits into
Conversation
|
Some changes occurred to MIR optimizations cc @rust-lang/wg-mir-opt |
This comment has been minimized.
This comment has been minimized.
920b7ec to
ef8a835
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
I think there are soundness flaws in this MIR transform. This program when run with Miri reports UB with the new pass: use std::future::Future;
use std::pin::{Pin, pin};
use std::task::{Context, Poll, Waker};
struct YieldOnce(bool);
impl Future for YieldOnce {
type Output = ();
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> {
if self.0 {
Poll::Ready(())
} else {
self.0 = true;
Poll::Pending
}
}
}
fn yield_once() -> YieldOnce {
YieldOnce(false)
}
fn block_on<F: Future>(f: F) -> F::Output {
let mut f = pin!(f);
let mut cx = Context::from_waker(Waker::noop());
loop {
if let Poll::Ready(v) = f.as_mut().poll(&mut cx) {
return v;
}
}
}
async fn a(arg: i32) -> i32 {
yield_once().await;
arg
}
async fn b(val: bool) -> i32 {
if val { a(1).await } else { a(-1).await }
}
fn main() {
block_on(b(true));
} |
… paths that shouldn't be translated
9f9270f to
d1fa25a
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. |
This comment has been minimized.
This comment has been minimized.
|
Alright, the miri issue should be resolved now @saethlin The problem was that we were moving locals before they were initialized. Now that's being checked using |
|
I have serious concerns about this pass as it is currently written:
I would recommend re-framing this as a basic block deduplication pass: if we can show that block A is identical to block B then we can redirect all control flow targeting block B to point to block A instead and eliminate block B. This achieves the desired effect of multiple The implementation should be done in several stages. Stage 1: Merge identical blocks onlyAt this stage, only blocks that have exactly the same statements and terminator can be merged. All fields and enum variants of the statements and terminators must be checked, as well as block-specific metadata like Stage 2: Merge identical blocks with different successorsThis extends stage 1 by merging blocks with different successors. This requires recursively proving that those successors are also identical. The current implementation using Once the worklist is empty, you have created a mapping of pairs of blocks that are known to be identical. You can now merge the pairs by redirecting all incoming control flow to a single block. Make sure to handle multiple groups of overlapping pairs correctly, I recommend using Stage 3: Merge identical blocks with different localsThis gets more complicated, but is still doable. When 2 statements differ only in the locals they use, you need to prove that you can merge those 2 locals into a single local. This is only possible if the 2 locals have the same type and their allocation lifetimes do not overlap. The The basic idea is to keep track of pairs of locals that need to be unified as you go through blocks. Before adding a pair of locals you must check their type and that neither overlaps with the other or any other local that has previously been merged with them. Once you have proved that you can unify the pairs of locals, then you've also proven that the blocks are identical, which makes it legal to merge them. You will likely need to perform the same alias fixup and storage statement reconstruction as Stage 4: Avoiding quadratic scanningThis is the part that I am less sure about, but at least it isn't necessary for correctness and can be deferred. The current algorithm scans the successors of every pair of yields, which ends up doing a lot of redundant work. It may instead be possible to iteratively merge common tail blocks by scanning backwards: unifying successors before predecessors allows block equivalences to expose further merge opportunities without repeatedly scanning sub-graphs of the function. At that point this becomes a general block deduplication optimization that is no longer specific to |
|
I would say that stage 1 and 2 are rarely going to be applicable due to how we lower |
|
But I have been thinking, whether we actually could do something during the MIR building phase. For |
|
Hey, thanks for the review! This is the first time touching Rust's MIR, so it would've been amazing if I did it correct the first time already. As for the complexity, I don't think that should be a huge problem since most functions don't have that many awaits. But I'll have to defer to the project on what's acceptable there. Too bad inserting moves is not correct. I did consider doing local unification, but that seemed more difficult and so I chose the way I did.
I think these stages aren't meant to be that useful on their own, but to split the work in separately reviewable chunks. So I think I will look into the proposed stages and eventually close this PR and open new ones for the stages. |
Would it make sense to have a dedicated |
It was more of a way to explain the optimization in a way that you can easily demonstrate its correctness. In terms of review, it's probably fine to do stage 1+2 together. |
Yes, @diondokter and I agreed and have believed that this would be more promising, but we took a more conservative approach which is to wrap the expression in a pseudo HIR node. That way the would be still freedom for us to maintain the expression lowering contained rather than dealing it at MIR runtime lowering: that would be much harder, I am afraid. |
Part of Async statemachine optimisation project goal
r? dingxiangfei2009
Added a pass that merges functionally identical yields right before the async StateTransform pass. The StateTransform pass creates a unique state for every yield, so by merging yields we reduce the amount of states in the state machines.
See the module documentation for more information.
As for how much binary size is saved, that's hard to say in general. There's plenty of code that doesn't have identical yields. But when there is, the savings can be huge. In this project it saves 616 bytes. For a customer, when they applied this pass manually, it saved ~2kb in one instance.
It'll also play nice with the next optimization I'm going to work on, where we optimize async functions with a single await.
I think the biggest risk in the PR are the compare functions for the basic blocks and their parts. That's most of the code in the pass and can break if the basic block structures are updated but this code is forgotten. I don't know how to solve/improve that, so I'm curious if anyone can think of something. The threat is that the compare functions say the blocks are identical, but they're not in reality.
Also, the pass is currently always enabled. I don't know what people want there. Does it need an unstable flag? Or depend on the mir opt level?