Skip to content
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

Merged
merged 4 commits into from
Nov 14, 2017

Conversation

djzin
Copy link
Contributor

@djzin djzin commented Nov 7, 2017

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.

@rust-highfive
Copy link
Collaborator

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.

@kennytm kennytm added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Nov 7, 2017
@nikomatsakis
Copy link
Contributor

cc @rust-lang/compiler -- Do we have any testing framework that could test this locally? I guess mir-opt wouldn't do it, we'd want to be checking the code that LLVM generates post-optimization.

@kennytm
Copy link
Member

kennytm commented Nov 7, 2017

@nikomatsakis src/test/codegen?

The existing src/test/codegen/vec-optimizes-away.rs seem to be a test like this.

@nikomatsakis
Copy link
Contributor

Yeah, so in Ye Olden Days we used to have a Switch that was targeted to enums, which handled this implicitly, but we removed it in favor of SwitchInt. I'm torn on whether this PR proposes the best fix -- it certainly aligns with what LLVM does but it feels a hair indirect. I wonder if it'd be nice to have a way on the SwitchInt to declare it to be exhaustive instead -- or perhaps to add "range" annotations to the temporary we use to store the result of the temporary?

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();
Copy link
Contributor

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.

Copy link
Contributor Author

@djzin djzin Nov 7, 2017

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 :)

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

@djzin djzin Nov 8, 2017

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@nikomatsakis
Copy link
Contributor

@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.

@nikomatsakis nikomatsakis reopened this Nov 7, 2017
@nikomatsakis
Copy link
Contributor

(Ooops, didn't mean to close.)

@djzin
Copy link
Contributor Author

djzin commented Nov 7, 2017

perhaps to add "range" annotations to the temporary we use to store the result of the temporary?

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;

define i8 @no_op(i8) {

    %temp = alloca i8
    store i8 %0, i8* %temp
    %value = load i8, i8* %temp, !range !{i8 0, i8 2}

    switch i8 %value, label %one [
        i8 0, label %zero
    ]

zero:
    br label %end

one:
    br label %end

end:
    %ret = phi i8 [0, %zero], [1, %one]
    ret i8 %ret
}

with opt -S -instcombine gives

define i8 @no_op(i8) {
  switch i8 %0, label %one [
    i8 0, label %zero
  ]

zero:                                             ; preds = %1
  br label %end

one:                                              ; preds = %1
  br label %end

end:                                              ; preds = %one, %zero
  %ret = phi i8 [ 0, %zero ], [ 1, %one ]
  ret i8 %ret
}

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.

@djzin
Copy link
Contributor Author

djzin commented Nov 7, 2017

The other thing we can do is add a bunch of @llvm.assume all over the place - consider the output 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

@nikomatsakis
Copy link
Contributor

@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.

@nikomatsakis
Copy link
Contributor

@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.)

@djzin
Copy link
Contributor Author

djzin commented Nov 13, 2017

made suggested changes

@nikomatsakis
Copy link
Contributor

@djzin thanks!

@nikomatsakis
Copy link
Contributor

@bors r+

@bors
Copy link
Contributor

bors commented Nov 14, 2017

📌 Commit 5b1cc1d has been approved by nikomatsakis

@kennytm kennytm added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Nov 14, 2017
bors added a commit that referenced this pull request Nov 14, 2017
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.
@bors
Copy link
Contributor

bors commented Nov 14, 2017

⌛ Testing commit 5b1cc1d with merge ff0f5de...

@bors
Copy link
Contributor

bors commented Nov 14, 2017

☀️ Test successful - status-appveyor, status-travis
Approved by: nikomatsakis
Pushing ff0f5de to master...

@bors bors merged commit 5b1cc1d into rust-lang:master Nov 14, 2017
@eddyb
Copy link
Member

eddyb commented Nov 18, 2017

Something very weird is happening here... The MIR we build (AFAICT, by looking at the files produced by -Zdump-mir, with the .-------.mir_map.0.mir suffix), without any extra MIR optimizations, for the testcase added in this PR, is branchless.

Not only that but so far I've been unable to find where this actually happens in rustc_mir.

EDIT: oh wow the test is bogus because of the overlapping imports, there is no match.

use Three::*;

pub enum Four { First, Second, Third, Fourth }
use Four::*;
Copy link
Member

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...

Copy link
Contributor Author

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...

Copy link
Member

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 matches 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.

Copy link
Contributor Author

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)

Deewiant added a commit to Deewiant/rust that referenced this pull request Dec 20, 2017
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.
bors added a commit that referenced this pull request Dec 21, 2017
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants