-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
always add an unreachable branch on matches to give more info to llvm #45821
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @nikomatsakis (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see the contribution instructions for more information. |
cc @rust-lang/compiler -- Do we have any testing framework that could test this locally? I guess |
@nikomatsakis src/test/codegen? The existing |
Yeah, so in Ye Olden Days we used to have a OTOH, I see the appeal of LLVM's approach. If you have some existing branches that become undefined behavior for whatever reason, it all falls out naturally. |
@@ -205,7 +205,9 @@ impl<'a, 'gcx, 'tcx> Builder<'a, 'gcx, 'tcx> { | |||
if let Some(otherwise_block) = otherwise_block { | |||
targets.push(otherwise_block); | |||
} else { | |||
values.pop(); | |||
let unreachable_block = self.cfg.start_new_block(); |
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.
Presuming we do decide to keep this approach, this particular patch doesn't feel like the best:
- It relies on the fact that
start_new_block
is returning a "unreachable" terminator by default. That feels like an implementation detail we may want to change. - It creates an extra block per match, when I suppose we only need one for the entire function, right?
I'd rather have something like self.cfg.unreachable_block()
that lazilly (or eagerly...) creates such a block.
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.
As I understand it, the blocks are not actually populated at all in this step. All of them are start_new_block
. The code that then follows on from this goes through each case and fills them in. It already handles the "last case is unreachable" case because it already has to handle
let x: u8;
match x {
0...255 => ...,
_ => ...
}
Please correct me if I'm wrong, I haven't worked with this code before :)
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 creates an extra block per match, when I suppose we only need one for the entire function, right?
Not sure what you mean here? Surely we could be matching different things within the same function
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.
So, I might have been remembering wrong in terms of how things work, I'll have to check -- but I don't think I am wrong that we only need one unreachable block per function. That is, why would we need more than one? All matches can share the same one, I should think.
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.
Ah I see what you mean now - I will investigate how many blocks get generated for a function with many matches.
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.
Hm have run into a snag with this. A Terminator
has a TerminatorKind
as well as a SourceInfo
. Since SourceInfo
necessarily differs between different matches, it is not possible to dedup the blocks, unless we put the wrong SourceInfo
on some (unreachable) basic blocks.
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.
So we do something similar for the cached_return_block
. The idea is that everybody references this block, which has no terminator as of yet, and then we assign the terminator with the source info of the function end:
https://github.com/rust-lang/rust/blob/master/src/librustc_mir/build/mod.rs#L400-L401
We might do something like that here.
@kennytm yep thanks =) I suppose I could have switched over to my terminal to check myself; but I also wanted to solicit opinion on the other bits of this PR. |
(Ooops, didn't mean to close.) |
The problem with "range" annotations is that they can only be used to annotate loads / function calls for some reason and so they tend to disappear. For example;
with
which leads to a lot of missed optimization opportunities. In fact I think we already do annotate "range" in this way, but it gets dropped. |
The other thing we can do is add a bunch of #[derive(Copy, Clone)]
pub enum Two { First, Second }
use Two::*;
#[no_mangle]
pub fn two_valued(x: Two) -> Two {
match x {
First => First,
Second => Second,
}
} which is define i8 @two_valued(i8) unnamed_addr #0 {
start:
%not.cond = icmp ne i8 %0, 0
%. = zext i1 %not.cond to i8
ret i8 %.
} two_valued:
testb %dil, %dil
setne %al
retq versus #[no_mangle]
pub fn two_valued(x: Two) -> Two {
unsafe {
std::intrinsics::assume(2 > x as u8);
}
match x {
First => First,
Second => Second,
}
} which gives define i8 @two_valued(i8) unnamed_addr #0 {
start:
%1 = icmp ult i8 %0, 2
tail call void @llvm.assume(i1 %1)
ret i8 %0
} two_valued:
movl %edi, %eax
retq |
@djzin well, so one question is how to tell LLVM that the match is exhaustive -- it seems like using an unreachable 'otherwise' block is fine there. The other question though is how to encode it in MIR. But I think I'm convinced we might as well use an unreachable block too. We can always change it after all. |
@djzin (just in case it was not clear, I think we're settled on taking this route that you've proposed, but still waiting on adding some codegen tests that show it is working, along with minor tweaks to the body of the PR.) |
made suggested changes |
… about which values are possible
651af1a
to
5b1cc1d
Compare
@djzin thanks! |
@bors r+ |
📌 Commit 5b1cc1d has been approved by |
always add an unreachable branch on matches to give more info to llvm As part of https://github.com/djzin/rustc-optimization I discovered that some simple enum optimizations (src/unary/three_valued_enum.rs and src/unary/four_valued_enum.rs in the repo) are not applied - and the reason for this is that we erase the info that the discriminant of an enum is one of the options by putting the last one in an "otherwise" branch. This patch adds an extra branch so that LLVM can know what the possibilities are for the discriminant, which fixes the three- and four- valued cases. Note that for whatever reason, this doesn't fix the case of 2 variants (most notably `Option` and `Result` have 2 variants) - a pass re-ordering might fix this or we may wish to add "assume" annotations on discriminants to force it to optimize.
☀️ Test successful - status-appveyor, status-travis |
Something very weird is happening here... The MIR we build (AFAICT, by looking at the files produced by Not only that but so far I've been unable to find where this actually happens in EDIT: oh wow the test is bogus because of the overlapping imports, there is no |
use Three::*; | ||
|
||
pub enum Four { First, Second, Third, Fourth } | ||
use Four::*; |
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'd be nice if we made a lint against this, set to error at least for the Rust codebase...
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.
wow thanks for catching this, I had them in 2 separate files before & just assumed that any warning would trap here...
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 are warnings from the match
es themselves but most tests are written without warning avoidance in mind so we'd have to do a lot of work to ban warnings from tests.
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.
Should probably as a matter of course put #![deny(warnings)]
on these tests where it is appropriate (at least I will do that in future)
Fixes rust-lang#46843. rust-lang#45821 added unreachable blocks in matches, which were terminated in construct_fn but not in construct_const, causing a panic due to "no terminator on block" when constants involved matching on enums. The "unimplemented expression type" error may go away in the future, the key is that we see the E0015 about using a non-const function and then don't ICE.
MIR: terminate unreachable blocks in construct_const Fixes #46843. #45821 added unreachable blocks in matches, which were terminated in construct_fn but not in construct_const, causing a panic due to "no terminator on block" when constants involved matching on enums. The "unimplemented expression type" error may go away in the future, the key is that we see the E0015 about using a non-const function and then don't ICE.
As part of https://github.com/djzin/rustc-optimization I discovered that some simple enum optimizations (src/unary/three_valued_enum.rs and src/unary/four_valued_enum.rs in the repo) are not applied - and the reason for this is that we erase the info that the discriminant of an enum is one of the options by putting the last one in an "otherwise" branch. This patch adds an extra branch so that LLVM can know what the possibilities are for the discriminant, which fixes the three- and four- valued cases.
Note that for whatever reason, this doesn't fix the case of 2 variants (most notably
Option
andResult
have 2 variants) - a pass re-ordering might fix this or we may wish to add "assume" annotations on discriminants to force it to optimize.