Skip to content

Commit 4abe247

Browse files
committed
Auto merge of #118796 - Nadrieril:fix-exponential-id-match-2, r=<try>
Exhaustiveness: Improve performance on wide matches #118437 revealed an exponential case in exhaustiveness checking. While [exponential cases are unavoidable](https://compilercrim.es/rust-np/), this one only showed up after my #117611 rewrite of the algorithm. I remember anticipating a case like this and dismissing it as unrealistic, but here we are :'). The tricky match is as follows: ```rust match command { BaseCommand { field01: true, .. } => {} BaseCommand { field02: true, .. } => {} BaseCommand { field03: true, .. } => {} BaseCommand { field04: true, .. } => {} BaseCommand { field05: true, .. } => {} BaseCommand { field06: true, .. } => {} BaseCommand { field07: true, .. } => {} BaseCommand { field08: true, .. } => {} BaseCommand { field09: true, .. } => {} BaseCommand { field10: true, .. } => {} // ...20 more of the same _ => {} } ``` To fix this, this PR formalizes a concept of "relevancy" (naming is hard) that was already used to decide what patterns to report. Now we track it for every row, which in wide matches like the above can drastically cut on the number of cases we explore. After this fix, the above match is checked with linear-many cases instead of exponentially-many. Fixes #118437 r? `@cjgillot`
2 parents c1a3919 + 13a8e36 commit 4abe247

File tree

2 files changed

+187
-29
lines changed

2 files changed

+187
-29
lines changed

compiler/rustc_mir_build/src/thir/pattern/usefulness.rs

+115-29
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,74 @@
299299
//!
300300
//!
301301
//!
302+
//! # `Missing` and relevant constructors
303+
//!
304+
//! Take the following example:
305+
//!
306+
//! ```compile_fail,E0004
307+
//! enum Direction { North, South, East, West }
308+
//! # let wind = (Direction::North, 0u8);
309+
//! match wind {
310+
//! (Direction::North, _) => {} // arm 1
311+
//! (_, 50..) => {} // arm 2
312+
//! }
313+
//! ```
314+
//!
315+
//! Remember that we represent the "everything else" cases with [`Constructor::Missing`]. When we
316+
//! specialize with `Missing` in the first column, we have one arm left:
317+
//!
318+
//! ```ignore(partial code)
319+
//! (50..) => {} // arm 2
320+
//! ```
321+
//!
322+
//! We then conclude that arm 2 is useful, and that the match is non-exhaustive with witness
323+
//! `(Missing, 0..50)` (which we would display to the user as `(_, 0..50)`).
324+
//!
325+
//! When we then specialize with `North`, we have two arms left:
326+
//!
327+
//! ```ignore(partial code)
328+
//! (_) => {} // arm 1
329+
//! (50..) => {} // arm 2
330+
//! ```
331+
//!
332+
//! Because `Missing` only matches wildcard rows, specializing with `Missing` is guaranteed to
333+
//! result in a subset of the rows obtained from specializing with anything else. This means that
334+
//! any row with a wildcard found useful when specializing with anything else would also be found
335+
//! useful in the `Missing` case. In our example, after specializing with `North` here we will not
336+
//! gain new information regarding the usefulness of arm 2 or of the fake wildcard row used for
337+
//! exhaustiveness. This allows us to skip cases.
338+
//!
339+
//! When specializing, if there is a `Missing` case we call the other constructors "irrelevant".
340+
//! When there is no `Missing` case there are no irrelevant constructors.
341+
//!
342+
//! What happens then is: when we specialize a wildcard with an irrelevant constructor, we know we
343+
//! won't get new info for this row; we consider that row "irrelevant". Whenever all the rows are
344+
//! found irrelevant, we can safely skip the case entirely.
345+
//!
346+
//! In the example above, we will entirely skip the `(North, 50..)` case. This skipping was
347+
//! developped as a solution to #118437. It doesn't look like much but it can save us from
348+
//! exponential blowup.
349+
//!
350+
//! There's a subtlety regarding exhaustiveness: while this shortcutting doesn't affect correctness,
351+
//! it can affect which witnesses are reported. For example, in the following:
352+
//!
353+
//! ```compile_fail,E0004
354+
//! # let foo = (true, true, true);
355+
//! match foo {
356+
//! (true, _, true) => {}
357+
//! (_, true, _) => {}
358+
//! }
359+
//! ```
360+
//!
361+
//! In this example we will skip the `(true, true, _)` case entirely. Thus `(true, true, false)`
362+
//! will not be reported as missing. In fact we go further than this: we deliberately do not report
363+
//! any cases that are irrelevant for the fake wildcard row. For example, in `match ... { (true,
364+
//! true) => {} }` we will not report `(true, false)` as missing. This was a deliberate choice made
365+
//! early in the development of rust; it so happens that it is beneficial for performance reasons
366+
//! too.
367+
//!
368+
//!
369+
//!
302370
//! # Or-patterns
303371
//!
304372
//! What we have described so far works well if there are no or-patterns. To handle them, if the
@@ -710,11 +778,15 @@ impl fmt::Display for ValidityConstraint {
710778
struct PatStack<'p, 'tcx> {
711779
// Rows of len 1 are very common, which is why `SmallVec[_; 2]` works well.
712780
pats: SmallVec<[&'p DeconstructedPat<'p, 'tcx>; 2]>,
781+
/// Sometimes we know that as far as this row is concerned, the current case is already handled
782+
/// by a different, more general, case. When all rows are irrelevant this allows us to skip many
783+
/// branches. This is purely an optimization. See at the top for details.
784+
relevant: bool,
713785
}
714786

715787
impl<'p, 'tcx> PatStack<'p, 'tcx> {
716788
fn from_pattern(pat: &'p DeconstructedPat<'p, 'tcx>) -> Self {
717-
PatStack { pats: smallvec![pat] }
789+
PatStack { pats: smallvec![pat], relevant: true }
718790
}
719791

720792
fn is_empty(&self) -> bool {
@@ -749,12 +821,17 @@ impl<'p, 'tcx> PatStack<'p, 'tcx> {
749821
&self,
750822
pcx: &PatCtxt<'_, 'p, 'tcx>,
751823
ctor: &Constructor<'tcx>,
824+
ctor_is_relevant: bool,
752825
) -> PatStack<'p, 'tcx> {
753826
// We pop the head pattern and push the new fields extracted from the arguments of
754827
// `self.head()`.
755828
let mut new_pats = self.head().specialize(pcx, ctor);
756829
new_pats.extend_from_slice(&self.pats[1..]);
757-
PatStack { pats: new_pats }
830+
// `ctor` is relevant for this row if it is the actual constructor of this row, or if the
831+
// row has a wildcard and `ctor` is relevant for wildcards.
832+
let ctor_is_relevant =
833+
!matches!(self.head().ctor(), Constructor::Wildcard) || ctor_is_relevant;
834+
PatStack { pats: new_pats, relevant: self.relevant && ctor_is_relevant }
758835
}
759836
}
760837

@@ -820,10 +897,11 @@ impl<'p, 'tcx> MatrixRow<'p, 'tcx> {
820897
&self,
821898
pcx: &PatCtxt<'_, 'p, 'tcx>,
822899
ctor: &Constructor<'tcx>,
900+
ctor_is_relevant: bool,
823901
parent_row: usize,
824902
) -> MatrixRow<'p, 'tcx> {
825903
MatrixRow {
826-
pats: self.pats.pop_head_constructor(pcx, ctor),
904+
pats: self.pats.pop_head_constructor(pcx, ctor, ctor_is_relevant),
827905
parent_row,
828906
is_under_guard: self.is_under_guard,
829907
useful: false,
@@ -952,8 +1030,9 @@ impl<'p, 'tcx> Matrix<'p, 'tcx> {
9521030
&self,
9531031
pcx: &PatCtxt<'_, 'p, 'tcx>,
9541032
ctor: &Constructor<'tcx>,
1033+
ctor_is_relevant: bool,
9551034
) -> Matrix<'p, 'tcx> {
956-
let wildcard_row = self.wildcard_row.pop_head_constructor(pcx, ctor);
1035+
let wildcard_row = self.wildcard_row.pop_head_constructor(pcx, ctor, ctor_is_relevant);
9571036
let new_validity = self.place_validity[0].specialize(pcx, ctor);
9581037
let new_place_validity = std::iter::repeat(new_validity)
9591038
.take(ctor.arity(pcx))
@@ -963,7 +1042,7 @@ impl<'p, 'tcx> Matrix<'p, 'tcx> {
9631042
Matrix { rows: Vec::new(), wildcard_row, place_validity: new_place_validity };
9641043
for (i, row) in self.rows().enumerate() {
9651044
if ctor.is_covered_by(pcx, row.head().ctor()) {
966-
let new_row = row.pop_head_constructor(pcx, ctor, i);
1045+
let new_row = row.pop_head_constructor(pcx, ctor, ctor_is_relevant, i);
9671046
matrix.expand_and_push(new_row);
9681047
}
9691048
}
@@ -1161,7 +1240,10 @@ impl<'tcx> WitnessMatrix<'tcx> {
11611240
if matches!(ctor, Constructor::Missing) {
11621241
// We got the special `Missing` constructor that stands for the constructors not present
11631242
// in the match.
1164-
if !report_individual_missing_ctors {
1243+
if missing_ctors.is_empty() {
1244+
// Nothing to report.
1245+
*self = Self::empty();
1246+
} else if !report_individual_missing_ctors {
11651247
// Report `_` as missing.
11661248
let pat = WitnessPat::wild_from_ctor(pcx, Constructor::Wildcard);
11671249
self.push_pattern(pat);
@@ -1220,6 +1302,15 @@ fn compute_exhaustiveness_and_usefulness<'p, 'tcx>(
12201302
) -> WitnessMatrix<'tcx> {
12211303
debug_assert!(matrix.rows().all(|r| r.len() == matrix.column_count()));
12221304

1305+
if !matrix.wildcard_row.relevant && matrix.rows().all(|r| !r.pats.relevant) {
1306+
// Here we know that nothing will contribute further to exhaustiveness or usefulness. This
1307+
// is purely an optimization: skipping this check doesn't affect correctness. This check
1308+
// does change runtime behavior from exponential to quadratic on some matches found in the
1309+
// wild, so it's pretty important. It also affects which missing patterns will be reported.
1310+
// See the top of the file for details.
1311+
return WitnessMatrix::empty();
1312+
}
1313+
12231314
let Some(ty) = matrix.head_ty() else {
12241315
// The base case: there are no columns in the matrix. We are morally pattern-matching on ().
12251316
// A row is useful iff it has no (unguarded) rows above it.
@@ -1232,8 +1323,14 @@ fn compute_exhaustiveness_and_usefulness<'p, 'tcx>(
12321323
return WitnessMatrix::empty();
12331324
}
12341325
}
1235-
// No (unguarded) rows, so the match is not exhaustive. We return a new witness.
1236-
return WitnessMatrix::unit_witness();
1326+
// No (unguarded) rows, so the match is not exhaustive. We return a new witness unless
1327+
// irrelevant.
1328+
return if matrix.wildcard_row.relevant {
1329+
WitnessMatrix::unit_witness()
1330+
} else {
1331+
// We can omit the witness without affecting correctness, so we do.
1332+
WitnessMatrix::empty()
1333+
};
12371334
};
12381335

12391336
debug!("ty: {ty:?}");
@@ -1274,32 +1371,21 @@ fn compute_exhaustiveness_and_usefulness<'p, 'tcx>(
12741371

12751372
let mut ret = WitnessMatrix::empty();
12761373
for ctor in split_ctors {
1277-
debug!("specialize({:?})", ctor);
12781374
// Dig into rows that match `ctor`.
1279-
let mut spec_matrix = matrix.specialize_constructor(pcx, &ctor);
1375+
debug!("specialize({:?})", ctor);
1376+
// `ctor` is *irrelevant* if there's another constructor in `split_ctors` that matches
1377+
// strictly fewer rows. In that case we can sometimes skip it. See the top of the file for
1378+
// details.
1379+
let ctor_is_relevant = matches!(ctor, Constructor::Missing) || missing_ctors.is_empty();
1380+
let mut spec_matrix = matrix.specialize_constructor(pcx, &ctor, ctor_is_relevant);
12801381
let mut witnesses = ensure_sufficient_stack(|| {
12811382
compute_exhaustiveness_and_usefulness(cx, &mut spec_matrix, false)
12821383
});
12831384

1284-
let counts_for_exhaustiveness = match ctor {
1285-
Constructor::Missing => !missing_ctors.is_empty(),
1286-
// If there are missing constructors we'll report those instead. Since `Missing` matches
1287-
// only the wildcard rows, it matches fewer rows than this constructor, and is therefore
1288-
// guaranteed to result in the same or more witnesses. So skipping this does not
1289-
// jeopardize correctness.
1290-
_ => missing_ctors.is_empty(),
1291-
};
1292-
if counts_for_exhaustiveness {
1293-
// Transform witnesses for `spec_matrix` into witnesses for `matrix`.
1294-
witnesses.apply_constructor(
1295-
pcx,
1296-
&missing_ctors,
1297-
&ctor,
1298-
report_individual_missing_ctors,
1299-
);
1300-
// Accumulate the found witnesses.
1301-
ret.extend(witnesses);
1302-
}
1385+
// Transform witnesses for `spec_matrix` into witnesses for `matrix`.
1386+
witnesses.apply_constructor(pcx, &missing_ctors, &ctor, report_individual_missing_ctors);
1387+
// Accumulate the found witnesses.
1388+
ret.extend(witnesses);
13031389

13041390
// A parent row is useful if any of its children is.
13051391
for child_row in spec_matrix.rows() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// check-pass
2+
struct BaseCommand {
3+
field01: bool,
4+
field02: bool,
5+
field03: bool,
6+
field04: bool,
7+
field05: bool,
8+
field06: bool,
9+
field07: bool,
10+
field08: bool,
11+
field09: bool,
12+
field10: bool,
13+
field11: bool,
14+
field12: bool,
15+
field13: bool,
16+
field14: bool,
17+
field15: bool,
18+
field16: bool,
19+
field17: bool,
20+
field18: bool,
21+
field19: bool,
22+
field20: bool,
23+
field21: bool,
24+
field22: bool,
25+
field23: bool,
26+
field24: bool,
27+
field25: bool,
28+
field26: bool,
29+
field27: bool,
30+
field28: bool,
31+
field29: bool,
32+
field30: bool,
33+
}
34+
35+
fn request_key(command: BaseCommand) {
36+
match command {
37+
BaseCommand { field01: true, .. } => {}
38+
BaseCommand { field02: true, .. } => {}
39+
BaseCommand { field03: true, .. } => {}
40+
BaseCommand { field04: true, .. } => {}
41+
BaseCommand { field05: true, .. } => {}
42+
BaseCommand { field06: true, .. } => {}
43+
BaseCommand { field07: true, .. } => {}
44+
BaseCommand { field08: true, .. } => {}
45+
BaseCommand { field09: true, .. } => {}
46+
BaseCommand { field10: true, .. } => {}
47+
BaseCommand { field11: true, .. } => {}
48+
BaseCommand { field12: true, .. } => {}
49+
BaseCommand { field13: true, .. } => {}
50+
BaseCommand { field14: true, .. } => {}
51+
BaseCommand { field15: true, .. } => {}
52+
BaseCommand { field16: true, .. } => {}
53+
BaseCommand { field17: true, .. } => {}
54+
BaseCommand { field18: true, .. } => {}
55+
BaseCommand { field19: true, .. } => {}
56+
BaseCommand { field20: true, .. } => {}
57+
BaseCommand { field21: true, .. } => {}
58+
BaseCommand { field22: true, .. } => {}
59+
BaseCommand { field23: true, .. } => {}
60+
BaseCommand { field24: true, .. } => {}
61+
BaseCommand { field25: true, .. } => {}
62+
BaseCommand { field26: true, .. } => {}
63+
BaseCommand { field27: true, .. } => {}
64+
BaseCommand { field28: true, .. } => {}
65+
BaseCommand { field29: true, .. } => {}
66+
BaseCommand { field30: true, .. } => {}
67+
68+
_ => {}
69+
}
70+
}
71+
72+
fn main() {}

0 commit comments

Comments
 (0)