Repository navigation
[mir] or-patterns duplicate the same branch #75439
Description
Activity
- addedA-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlA-patternsRelating to patterns and pattern matchingRelating to patterns and pattern matchingF-or_patterns`#![feature(or_patterns)]``#![feature(or_patterns)]`
on Aug 12, 2020 - addedC-enhancementCategory: An issue proposing an enhancement or a PR with one.Category: An issue proposing an enhancement or a PR with one.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler team, which will review and decide on the PR/issue.
on Aug 12, 2020 Was this resolved by #75537? I believe that this would actually match on it, just because all the statements are the same
I've just checked. The MIR isn't improved with #75537 .
Reacted by Julian KnodtAh woops yea I can't count, this has 3 branches not 2, which is the only permitted number of branches being optimized currently.
- added 4 commits that reference this issue
on Sep 4, 2020 3 remaining items
@rustbot claim
I'm not sure if I'll be able to fix this, but I spent some time this weekend studying lowering to MIR and tried to understand the issue here.
First of all, the inefficiency here exists even in the final LLVM (with
-O), so I think this may worth fixing. LLVM for the original repro:issue75439 $ rustc +stage1_2 test.rs --crate-type=rlib --emit=llvm-ir -O issue75439 $ cat test.ll ; ModuleID = 'test.3a1fbbbh-cgu.0' source_filename = "test.3a1fbbbh-cgu.0" target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu" ; test::foo3 ; Function Attrs: norecurse nounwind nonlazybind readnone uwtable define i40 @_ZN4test4foo317h93f9bb6eba5df219E(i128 %0) unnamed_addr #0 { start: %1 = and i128 %0, 18446744073709551615 %2 = icmp eq i128 %1, 0 br i1 %2, label %bb3, label %bb9 bb3: ; preds = %start %_3.sroa.5.0.extract.shift = lshr i128 %0, 64 %_3.sroa.5.0.extract.trunc = trunc i128 %_3.sroa.5.0.extract.shift to i32 switch i32 %_3.sroa.5.0.extract.trunc, label %bb9 [ i32 0, label %bb6 i32 -65536, label %bb7 ] bb6: ; preds = %bb3 br label %bb9 bb7: ; preds = %bb3 br label %bb9 bb9: ; preds = %bb7, %bb6, %start, %bb3 %.sroa.0.0 = phi i40 [ 0, %bb3 ], [ 0, %start ], [ 1, %bb6 ], [ 1, %bb7 ] %3 = lshr i128 %0, 88 %4 = trunc i128 %3 to i40 %.sroa.3.0.insert.shift = and i40 %4, -256 %.sroa.0.0.insert.insert = or i40 %.sroa.0.0, %.sroa.3.0.insert.shift ret i40 %.sroa.0.0.insert.insert } attributes #0 = { norecurse nounwind nonlazybind readnone uwtable "probe-stack"="__rust_probestack" "target-cpu"="x86-64" } !llvm.module.flags = !{!0, !1} !0 = !{i32 7, !"PIC Level", i32 2} !1 = !{i32 2, !"RtLibUseGOT", i32 1}bb6andbb7are the targets of the or pattern and they're duplicates. I'm not sure why LLVM can't eliminate those.Now, I looked at why these duplicate blocks are generated, in MIR. I believe the reason is the same as the reason why we generate duplicate blocks in this simpler program:
pub fn test(dwords: [u32; 2]) { match dwords { [0 | 1, _a] => (), _ => (), } }
LLVM is able to optimize the duplicate blocks in this program, but in MIR we still have duplication very similar to the original repro:
fn test(_1: [u32; 2]) -> () { debug dwords => _1; let mut _0: (); let _2: u32; scope 1 { debug _a => _2; } bb0: { switchInt(_1[0 of 2]) -> [0_u32: bb2, 1_u32: bb3, otherwise: bb4]; } bb1: { StorageDead(_2); goto -> bb4; } bb2: { StorageLive(_2); _2 = _1[1 of 2]; goto -> bb1; } bb3: { StorageLive(_2); _2 = _1[1 of 2]; goto -> bb1; } bb4: { return; } }Namely
bb2andbb3.I think if we could generate better MIR for this simpler program that would also fix the problem with the original repro.
Here's the original MIR for this program (before any passes):
fn test(_1: [u32; 2]) -> () { debug dwords => _1; let mut _0: (); let _2: u32; scope 1 { debug _a => _2; } bb0: { FakeRead(ForMatchedPlace, _1); switchInt(_1[0 of 2]) -> [0_u32: bb1, 1_u32: bb2, otherwise: bb3]; } bb1: { falseEdge -> [real: bb5, imaginary: bb2]; } bb2: { falseEdge -> [real: bb6, imaginary: bb3]; } bb3: { _0 = (); goto -> bb7; } bb4: { _0 = (); StorageDead(_2); goto -> bb7; } bb5: { StorageLive(_2); _2 = _1[1 of 2]; goto -> bb4; } bb6: { StorageLive(_2); _2 = _1[1 of 2]; goto -> bb4; } bb7: { return; } }The reason for this duplication is: originally for this program, in the first arm, we have a candidate with two "match pairs": the or pattern, and the binding (
_a). We simplify this (simplify_candidate) to a candidate with a binding (_a) and two subcandidates, for the branches of the or pattern.When generating MIR for a subcandidate, we first generate a block just to be able to chain blocks of arms for borrow checking (using "false eges"). This is why we create
bb5when generatingbb1, andbb6when generatingbb2.Then in those new blocks (
bb5andbb6) we introduce the bindings of the parent candidate, with some extra stuff for ascriptions and guards. This generates the duplicate codeStorageLive(_2); _2 = _1[1 of 2]; goto -> ...;for all alternatives of the or pattern.
(The intermediate blocks
bb1andbb2are removed afterSimplifyBranchesand the nextSimplifyCfg)I think it's possible to generate better code here when the subcandidates (transitively) don't have any bindings, ascriptions, and guards (I don't understand the guards in details yet so I don't know if there are room for optimization also). In those cases, before generating MIR for the subcandidates, we create a "target" block where we will generate the binding code. Then create branches for each subcandidate as we do today (with the false edges), but these subcandidates jump to the "target" block instead of to fresh blocks. The target block will then have the code for the binding in the parent candidate.
In other words we generate one block for the parent candidate's bindings instead of introducing the bindings it in each subcandidate.
I actually have an implementation of this, but it causes linker errors when compiling libraries. I'll submit a PR once it's working.
One problem is the idea is the check "do the subcandidates have any bindings, ascriptions, or guards" requires traversing the entire candidate tree. I don't know if we can implement this more efficiently somehow.
The MIR (before passes) with this implementation would look like:
fn test(_1: [u32; 2]) -> () { debug dwords => _1; let mut _0: (); let _2: u32; scope 1 { debug _a => _2; } bb0: { FakeRead(ForMatchedPlace, _1); switchInt(_1[0 of 2]) -> [0_u32: bb1, 1_u32: bb2, otherwise: bb3]; } bb1: { falseEdge -> [real: bb5, imaginary: bb2]; } bb2: { falseEdge -> [real: bb5, imaginary: bb3]; } bb3: { _0 = (); goto -> bb6; } bb4: { _0 = (); StorageDead(_2); goto -> bb6; } bb5: { StorageLive(_2); _2 = _1[1 of 2]; goto -> bb4; } bb6: { return; } }Because we no longer have duplicate blocks for or pattern alternatives the issue is fixed.
It looks like you already figured out most of this, but here some notes I wrote from the last time I went through some of that code: rust-lang/rustc-dev-guide#943
Reacted by Ömer Sinan AğacanReacted by NadrierilThanks @mark-i-m, that's very helpful.
Update: I figured this out and have a working prototype. On the way I was also able to come up with examples where the duplication is much larger then the original repro or my simpler repro above, which my code fixes. I'm hoping to submit a PR this weekend.
Reacted by tesuji and markReacted by markUpdate: my patch works mostly fine. There are two failures in the test suite, both are caused by disappeared
StorageDeadinstructions after merging the duplicate blocks. I don't make any changes related to drop scheduling so I'm not sure what the reason is. I asked a question about this on Zulip. Once that's fixed it will be passing the test suite and I'll submit a PR.In the meantime, if anyone knows about the question I linked above, that would be very helpful.
Reacted by mark- addedC-optimizationCategory: An issue highlighting optimization opportunities or PRs implementing suchCategory: An issue highlighting optimization opportunities or PRs implementing such
on Oct 8, 2023 One problem is the idea is the check "do the subcandidates have any bindings, ascriptions, or guards" requires traversing the entire candidate tree.
Isn't that already what we do in
merge_trivial_subcandidatesthough? My surface understanding is thatmerge_trivial_subcandidatesshould solve exactly this, but clearly it doesn't :/since this is reviving a
long deadold post, you probably want to double check this behavior is still happening, and close this issue if not.Even if it's not still happening, it might be good to close the issue and start a new one so it's more easily tracked (and doesn't ping people who aren't working on it actively anymore)
I did check, it's still duplicated
This fixed by #123067. cc @Nadrieril
@rustbot label +E-needs-test
Reacted by Nadrieril, ptrca and Julian Knodt- addedE-needs-testCall for participation: An issue has been fixed and does not reproduce, but no test has been added.Call for participation: An issue has been fixed and does not reproduce, but no test has been added.
on Aug 31, 2024 Amazing, thanks for figuring this out
The fixing PR already updates the test for this issue so this can be closed
Consider this snippet:
The or-pattern
0 | FFhas this MIR:While
bb6andbb7points to the same branch:I would expect the or-pattern points the same branch.