Skip to content

Commit 5fc4c47

Browse files
committed
Improve performance on wide matches
1 parent 57ad505 commit 5fc4c47

File tree

2 files changed

+187
-29
lines changed

2 files changed

+187
-29
lines changed

compiler/rustc_pattern_analysis/src/usefulness.rs

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

679751
impl<'a, 'p, Cx: TypeCx> PatStack<'a, 'p, Cx> {
680752
fn from_pattern(pat: &'a DeconstructedPat<'p, Cx>) -> Self {
681-
PatStack { pats: smallvec![pat] }
753+
PatStack { pats: smallvec![pat], relevant: true }
682754
}
683755

684756
fn is_empty(&self) -> bool {
@@ -713,12 +785,17 @@ impl<'a, 'p, Cx: TypeCx> PatStack<'a, 'p, Cx> {
713785
&self,
714786
pcx: &PlaceCtxt<'a, 'p, Cx>,
715787
ctor: &Constructor<Cx>,
788+
ctor_is_relevant: bool,
716789
) -> PatStack<'a, 'p, Cx> {
717790
// We pop the head pattern and push the new fields extracted from the arguments of
718791
// `self.head()`.
719792
let mut new_pats = self.head().specialize(pcx, ctor);
720793
new_pats.extend_from_slice(&self.pats[1..]);
721-
PatStack { pats: new_pats }
794+
// `ctor` is relevant for this row if it is the actual constructor of this row, or if the
795+
// row has a wildcard and `ctor` is relevant for wildcards.
796+
let ctor_is_relevant =
797+
!matches!(self.head().ctor(), Constructor::Wildcard) || ctor_is_relevant;
798+
PatStack { pats: new_pats, relevant: self.relevant && ctor_is_relevant }
722799
}
723800
}
724801

@@ -784,10 +861,11 @@ impl<'a, 'p, Cx: TypeCx> MatrixRow<'a, 'p, Cx> {
784861
&self,
785862
pcx: &PlaceCtxt<'a, 'p, Cx>,
786863
ctor: &Constructor<Cx>,
864+
ctor_is_relevant: bool,
787865
parent_row: usize,
788866
) -> MatrixRow<'a, 'p, Cx> {
789867
MatrixRow {
790-
pats: self.pats.pop_head_constructor(pcx, ctor),
868+
pats: self.pats.pop_head_constructor(pcx, ctor, ctor_is_relevant),
791869
parent_row,
792870
is_under_guard: self.is_under_guard,
793871
useful: false,
@@ -913,8 +991,9 @@ impl<'a, 'p, Cx: TypeCx> Matrix<'a, 'p, Cx> {
913991
&self,
914992
pcx: &PlaceCtxt<'a, 'p, Cx>,
915993
ctor: &Constructor<Cx>,
994+
ctor_is_relevant: bool,
916995
) -> Matrix<'a, 'p, Cx> {
917-
let wildcard_row = self.wildcard_row.pop_head_constructor(pcx, ctor);
996+
let wildcard_row = self.wildcard_row.pop_head_constructor(pcx, ctor, ctor_is_relevant);
918997
let new_validity = self.place_validity[0].specialize(ctor);
919998
let new_place_validity = std::iter::repeat(new_validity)
920999
.take(ctor.arity(pcx))
@@ -924,7 +1003,7 @@ impl<'a, 'p, Cx: TypeCx> Matrix<'a, 'p, Cx> {
9241003
Matrix { rows: Vec::new(), wildcard_row, place_validity: new_place_validity };
9251004
for (i, row) in self.rows().enumerate() {
9261005
if ctor.is_covered_by(pcx, row.head().ctor()) {
927-
let new_row = row.pop_head_constructor(pcx, ctor, i);
1006+
let new_row = row.pop_head_constructor(pcx, ctor, ctor_is_relevant, i);
9281007
matrix.expand_and_push(new_row);
9291008
}
9301009
}
@@ -1122,7 +1201,10 @@ impl<Cx: TypeCx> WitnessMatrix<Cx> {
11221201
if matches!(ctor, Constructor::Missing) {
11231202
// We got the special `Missing` constructor that stands for the constructors not present
11241203
// in the match.
1125-
if !report_individual_missing_ctors {
1204+
if missing_ctors.is_empty() {
1205+
// Nothing to report.
1206+
*self = Self::empty();
1207+
} else if !report_individual_missing_ctors {
11261208
// Report `_` as missing.
11271209
let pat = WitnessPat::wild_from_ctor(pcx, Constructor::Wildcard);
11281210
self.push_pattern(pat);
@@ -1181,6 +1263,15 @@ fn compute_exhaustiveness_and_usefulness<'a, 'p, Cx: TypeCx>(
11811263
) -> WitnessMatrix<Cx> {
11821264
debug_assert!(matrix.rows().all(|r| r.len() == matrix.column_count()));
11831265

1266+
if !matrix.wildcard_row.relevant && matrix.rows().all(|r| !r.pats.relevant) {
1267+
// Here we know that nothing will contribute further to exhaustiveness or usefulness. This
1268+
// is purely an optimization: skipping this check doesn't affect correctness. This check
1269+
// does change runtime behavior from exponential to quadratic on some matches found in the
1270+
// wild, so it's pretty important. It also affects which missing patterns will be reported.
1271+
// See the top of the file for details.
1272+
return WitnessMatrix::empty();
1273+
}
1274+
11841275
let Some(ty) = matrix.head_ty() else {
11851276
// The base case: there are no columns in the matrix. We are morally pattern-matching on ().
11861277
// A row is useful iff it has no (unguarded) rows above it.
@@ -1193,8 +1284,14 @@ fn compute_exhaustiveness_and_usefulness<'a, 'p, Cx: TypeCx>(
11931284
return WitnessMatrix::empty();
11941285
}
11951286
}
1196-
// No (unguarded) rows, so the match is not exhaustive. We return a new witness.
1197-
return WitnessMatrix::unit_witness();
1287+
// No (unguarded) rows, so the match is not exhaustive. We return a new witness unless
1288+
// irrelevant.
1289+
return if matrix.wildcard_row.relevant {
1290+
WitnessMatrix::unit_witness()
1291+
} else {
1292+
// We can omit the witness without affecting correctness, so we do.
1293+
WitnessMatrix::empty()
1294+
};
11981295
};
11991296

12001297
debug!("ty: {ty:?}");
@@ -1237,32 +1334,21 @@ fn compute_exhaustiveness_and_usefulness<'a, 'p, Cx: TypeCx>(
12371334

12381335
let mut ret = WitnessMatrix::empty();
12391336
for ctor in split_ctors {
1240-
debug!("specialize({:?})", ctor);
12411337
// Dig into rows that match `ctor`.
1242-
let mut spec_matrix = matrix.specialize_constructor(pcx, &ctor);
1338+
debug!("specialize({:?})", ctor);
1339+
// `ctor` is *irrelevant* if there's another constructor in `split_ctors` that matches
1340+
// strictly fewer rows. In that case we can sometimes skip it. See the top of the file for
1341+
// details.
1342+
let ctor_is_relevant = matches!(ctor, Constructor::Missing) || missing_ctors.is_empty();
1343+
let mut spec_matrix = matrix.specialize_constructor(pcx, &ctor, ctor_is_relevant);
12431344
let mut witnesses = ensure_sufficient_stack(|| {
12441345
compute_exhaustiveness_and_usefulness(mcx, &mut spec_matrix, false)
12451346
});
12461347

1247-
let counts_for_exhaustiveness = match ctor {
1248-
Constructor::Missing => !missing_ctors.is_empty(),
1249-
// If there are missing constructors we'll report those instead. Since `Missing` matches
1250-
// only the wildcard rows, it matches fewer rows than this constructor, and is therefore
1251-
// guaranteed to result in the same or more witnesses. So skipping this does not
1252-
// jeopardize correctness.
1253-
_ => missing_ctors.is_empty(),
1254-
};
1255-
if counts_for_exhaustiveness {
1256-
// Transform witnesses for `spec_matrix` into witnesses for `matrix`.
1257-
witnesses.apply_constructor(
1258-
pcx,
1259-
&missing_ctors,
1260-
&ctor,
1261-
report_individual_missing_ctors,
1262-
);
1263-
// Accumulate the found witnesses.
1264-
ret.extend(witnesses);
1265-
}
1348+
// Transform witnesses for `spec_matrix` into witnesses for `matrix`.
1349+
witnesses.apply_constructor(pcx, &missing_ctors, &ctor, report_individual_missing_ctors);
1350+
// Accumulate the found witnesses.
1351+
ret.extend(witnesses);
12661352

12671353
// A parent row is useful if any of its children is.
12681354
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)