-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Add dataflow analysis of enum variants #96991
Conversation
r? @nagisa (rust-highfive has picked a reviewer for you, use r? to override) |
This comment has been minimized.
This comment has been minimized.
7b2fb12
to
987bd34
Compare
This comment has been minimized.
This comment has been minimized.
e3027e7
to
be51dcf
Compare
This comment has been minimized.
This comment has been minimized.
a39d410
to
1b69305
Compare
ultra nit: I like line breaks around functions. |
@bors try @rust-timer queue |
Awaiting bors try build completion. @rustbot label: +S-waiting-on-perf |
⌛ Trying commit 1b69305eac7d333c321c35e810a5303c49d1dec2 with merge 20dd8008d52592f40dfc3aa396d7f10ce1dcda5c... |
@lqd will a perf run work if CI failed? |
This comment has been minimized.
This comment has been minimized.
Usually yes; this time, probably not :) |
This comment has been minimized.
This comment has been minimized.
@lqd now mir-opt tests are failing (since I didn't |
This comment has been minimized.
This comment has been minimized.
I'm going to pass on this PR over to r? @oli-obk given that this is closely related to constant propagation/evaluation. I don't really have enough capacity to really dig into the implementation here. My observation broadly is that it isn't entirely clear if this should be a separate pass or a part of a larger constant propagation pass. Having it be separate seems like it is going to potentially require repeated runs of this pass interleaved with other passes in order to obtain meaningful IR quality improvements. That usually isn't great for compilation performance. |
If possible, I'd like to get a chance to review this as well, just to check soundness and interaction with unsafe code, MIR semantics, etc. I'll do my best to make sure that it's before the rest of review is completed, so that nothing is blocked on me. (Actually, is there a way that I can ask to be cc'd on all new MIR opts/significant changes to existing ones?) |
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.
The direction here is definitely the right one. There are some soundness issues that need to be fixed though. More importantly, I'd also like the documentation to be fleshed out. With these types of analyses its important that we are very explicit about exactly what conclusions we are drawing, and the line of inference we are using to get there. I've noted a couple places where I think this could be done.
Edit: Forgot to mention, but all the below code snippets were compiled with
rustc +stage1 -C opt-level=0 -Z mir-opt-level=0 -Z mir-enable-passes=+SingleEnum -Z dump-mir=all test.rs
if let Some(rhs_local) = rhs.local_or_deref_local() { | ||
state.get(rhs_local).map(|f| f.1).copied() | ||
} else { | ||
rhs.ty(self.body, self.tcx).variant_index.map(|var_idx| var_idx) |
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.
rhs.ty(self.body, self.tcx).variant_index.map(|var_idx| var_idx) | |
None |
Afaik this is always None
already anyway. I do want to investigate adding variants as types in MIR at some point (for a bunch of reasons), but we don't have that today.
Also, even if this is not None
, the semantics here are different from what const eval does - const eval just ignores the variant index. That means that if we did want to support having a variant index in "top level" types like this, we need to adjust Miri to also check that the validity invariant of that variant is actually upheld.
// Assigning a constant does not affect discriminant? | ||
Operand::Constant(_c) => return, |
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 certainly can. Consider:
#![feature(inline_const)]
enum E {
A,
B,
}
fn main() {
dbg!(foo());
}
#[inline(never)]
fn foo() -> u32 {
let mut x = E::A;
x = const { E::B };
match x {
E::A => 1,
E::B => 2,
}
}
Ideally you would be able to compute the discriminant of the const here so that you can introduce the correct fact - I'm not so sure how to do this though, maybe @oli-obk knows?
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.
for now I just killed the fact, const prop will likely handle this optimization but I can put it back in the future
|
||
if let Some(new_fact) = new_fact { | ||
let loc = Location { block: target.target, statement_index: 0 }; | ||
state.insert(local, loc, new_fact); |
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 think things are slightly mixed up here. In MIR like this:
_5 = Discrim(_8);
switchInt(_5, 0 => bb3, otherwise: bb5)
Your local
will store _5
, but really you want to insert a new fact about _8
. Take a look at https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir_dataflow/impls/fn.switch_on_enum_discriminant.html for inspiration on how to do this kind of thing. Essentially, you will just want to check if the previous statement was an assignment of the shape given above.
@@ -11,7 +11,7 @@ fn main() { | |||
} | |||
// ref binding to non-copy value and or-pattern | |||
let (MyEnum::A(ref x) | MyEnum::B { f: ref x }) = (MyEnum::B { f: String::new() }) else { | |||
panic!(); | |||
panic!("Shouldn't have matched enum"); |
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.
What prompted the changes?
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 couldn't tell which one was matching the panic, I forget why line numbers weren't sufficient, but this was miscompiling so I had to check where it was miscompiling.
Some changes occurred to MIR optimizations cc @rust-lang/wg-mir-opt |
Constify `Discriminant` Fix wrong size scalar in const_from_scalar Kill aliasable enums bless mir Praise the mir Switch to FxHashMap Rm bots Attempt btreemap instead of hash Maybe there were issues with queries? Reattempt index vec Implement cache for dataflow analysis Add test Move single enum after const prop
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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.
Some more thoughts about documentation
pub struct FactCache<I, L, F, const N: usize> { | ||
facts: [F; N], | ||
ord: [(I, L); N], | ||
len: usize, | ||
} |
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 for the documentation update. What is the partial order under which this is a lattice? If this is a well-known result from theory (I'm not terribly familar) feel free to just provide a link to a reference
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.
there's no theory, it's just that each fact gets inserted at a specific location ( body + stmt idx ), so I attempt to keep later facts more than more recent ones, like an LRU cache.
What I mean is that we could represent the entire set of facts using a HashMap<Idx, Value>
, but this will grow linearly with the size of the program (or at least whatever is being analyzed). Instead, to use constant space, we keep a set of facts that evicts older ones, which we assume are no longer relevant, and replaces them with the most recent updates. This a strict subset of the HashMap<Idx, Value>
, so should also be a lattice.
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
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.
Ok, yeah, sorry, what I said before was silly. Yes, the subset partial order is an option, but it's not clear to me that that results in a lattice. Under the subset partial order the least upper bound of two states each storing N
facts is a state that would have to store 2*N
facts, which you can't represent
I should start thinking before I say things. Yeah, ok, this does seem like a lattice - let me go and re-read some stuff then
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.
Yeah, ok I thought about this a bunch more and I think it's ok. We can imagine this fact cache as implicitly storing
enum LocalResult {
Top,
Fact(F, L),
Bot(L),
}
for each I
. Locals which do not have an entry in the fact cache are implicitly Top
. The partial order within each LocalResult
is then Bot < Fact(F, min) < ... < Fact(F, max) < Top
for each F
, and the partial order for the fact cache as a whole is then the product partial order. This does indeed form a lattice. This should be documented though.
Add many of the additional lattice methods with comments, as well as clean up some issues which did not appear in the tests (i.e. SwitchIntEdgeEffect).
I think I'd like to see the implementation of the enum VariantFact {
Top,
Variant(VariantIdx),
Bot,
} This should make things much easier to test. I wouldn't worry about performance for the initial version - we'll turn it off by default, so it only has to perform well enough to get through its tests. We can then hook it up with smarter domains and other performance optimizations later, once the parts are sound on their own. |
I can break them into separate commits if you'd like, but breaking them into separate PRs would be quite slow since it would require re-review. |
I do think splitting this up would be a good idea. I can review both PRs, and that will be easier for me, not harder, especially because it'll be easier to reproduce bugs if we separate out the simpler part. I would also like to see some more justification for the need to reduce the optimization power of this pass over the "fully precise" version for perf reasons. We should first try to do all the perf improvements we can without resorting to that (I'm happy to help with this) |
@JulianKnodt |
I haven't had time to update this for quite a while, I may get back to it at some point but am working on other things |
@JulianKnodt Please reopen when you are ready to continue with this. @rustbot label: +S-inactive |
This adds a dataflow analysis in MIR, in the case that at compile time it is known that a variable will have a specific value for an enum, it converts
Discriminant
calls to constants. There seemed to be a number of issues where codegen could not remove matches when a constant was known, so adding an explicit analysis in MIR should help give more control.I still need to add tests that it works, but it seems to apply to a number of tests already. If possible, a perf-run would be appreciated.