diff options
| author | Valerii Lashmanov <vflashm@gmail.com> | 2020-09-15 09:37:19 -0500 |
|---|---|---|
| committer | Valerii Lashmanov <vflashm@gmail.com> | 2020-09-17 20:44:11 -0500 |
| commit | 2f3296192bb5be5fcb02975395052ee8c3b2bd68 (patch) | |
| tree | 01176084735b315f8535a3152fbf2ce41fe54e5a | |
| parent | 255ceeb5ff9875b7f525aa101c8adc155f3e0ef8 (diff) | |
| download | rust-2f3296192bb5be5fcb02975395052ee8c3b2bd68.tar.gz rust-2f3296192bb5be5fcb02975395052ee8c3b2bd68.zip | |
Only visit types once when walking the type tree
This fixes #72408. Nested closures were resulting in exponential compilation time. As a performance optimization this change introduces MiniSet, which is a simple small storage optimized set.
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | compiler/rustc_infer/src/infer/outlives/verify.rs | 40 | ||||
| -rw-r--r-- | compiler/rustc_middle/Cargo.toml | 1 | ||||
| -rw-r--r-- | compiler/rustc_middle/src/ty/outlives.rs | 33 | ||||
| -rw-r--r-- | compiler/rustc_middle/src/ty/walk.rs | 80 | ||||
| -rw-r--r-- | src/test/ui/closures/issue-72408-nested-closures-exponential.rs | 59 | ||||
| -rw-r--r-- | src/test/ui/issues/issue-22638.rs | 2 | ||||
| -rw-r--r-- | src/test/ui/issues/issue-22638.stderr | 12 | ||||
| -rw-r--r-- | src/test/ui/type_length_limit.rs | 2 | ||||
| -rw-r--r-- | src/test/ui/type_length_limit.stderr | 2 |
10 files changed, 193 insertions, 39 deletions
diff --git a/Cargo.lock b/Cargo.lock index 9b5a4c24d25..05d3a9c5621 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3733,6 +3733,7 @@ dependencies = [ name = "rustc_middle" version = "0.0.0" dependencies = [ + "arrayvec", "bitflags", "chalk-ir", "measureme", diff --git a/compiler/rustc_infer/src/infer/outlives/verify.rs b/compiler/rustc_infer/src/infer/outlives/verify.rs index d6f1ca3cf95..e06bfb59580 100644 --- a/compiler/rustc_infer/src/infer/outlives/verify.rs +++ b/compiler/rustc_infer/src/infer/outlives/verify.rs @@ -3,6 +3,7 @@ use crate::infer::{GenericKind, VerifyBound}; use rustc_data_structures::captures::Captures; use rustc_hir::def_id::DefId; use rustc_middle::ty::subst::{GenericArg, GenericArgKind, Subst}; +use rustc_middle::ty::walk::MiniSet; use rustc_middle::ty::{self, Ty, TyCtxt}; /// The `TypeOutlives` struct has the job of "lowering" a `T: 'a` @@ -31,16 +32,23 @@ impl<'cx, 'tcx> VerifyBoundCx<'cx, 'tcx> { /// Returns a "verify bound" that encodes what we know about /// `generic` and the regions it outlives. pub fn generic_bound(&self, generic: GenericKind<'tcx>) -> VerifyBound<'tcx> { + let mut visited = MiniSet::new(); match generic { GenericKind::Param(param_ty) => self.param_bound(param_ty), - GenericKind::Projection(projection_ty) => self.projection_bound(projection_ty), + GenericKind::Projection(projection_ty) => { + self.projection_bound(projection_ty, &mut visited) + } } } - fn type_bound(&self, ty: Ty<'tcx>) -> VerifyBound<'tcx> { + fn type_bound( + &self, + ty: Ty<'tcx>, + visited: &mut MiniSet<GenericArg<'tcx>>, + ) -> VerifyBound<'tcx> { match *ty.kind() { ty::Param(p) => self.param_bound(p), - ty::Projection(data) => self.projection_bound(data), + ty::Projection(data) => self.projection_bound(data, visited), ty::FnDef(_, substs) => { // HACK(eddyb) ignore lifetimes found shallowly in `substs`. // This is inconsistent with `ty::Adt` (including all substs), @@ -50,9 +58,9 @@ impl<'cx, 'tcx> VerifyBoundCx<'cx, 'tcx> { let mut bounds = substs .iter() .filter_map(|child| match child.unpack() { - GenericArgKind::Type(ty) => Some(self.type_bound(ty)), + GenericArgKind::Type(ty) => Some(self.type_bound(ty, visited)), GenericArgKind::Lifetime(_) => None, - GenericArgKind::Const(_) => Some(self.recursive_bound(child)), + GenericArgKind::Const(_) => Some(self.recursive_bound(child, visited)), }) .filter(|bound| { // Remove bounds that must hold, since they are not interesting. @@ -66,7 +74,7 @@ impl<'cx, 'tcx> VerifyBoundCx<'cx, 'tcx> { ), } } - _ => self.recursive_bound(ty.into()), + _ => self.recursive_bound(ty.into(), visited), } } @@ -137,7 +145,11 @@ impl<'cx, 'tcx> VerifyBoundCx<'cx, 'tcx> { self.declared_projection_bounds_from_trait(projection_ty) } - pub fn projection_bound(&self, projection_ty: ty::ProjectionTy<'tcx>) -> VerifyBound<'tcx> { + pub fn projection_bound( + &self, + projection_ty: ty::ProjectionTy<'tcx>, + visited: &mut MiniSet<GenericArg<'tcx>>, + ) -> VerifyBound<'tcx> { debug!("projection_bound(projection_ty={:?})", projection_ty); let projection_ty_as_ty = @@ -166,21 +178,25 @@ impl<'cx, 'tcx> VerifyBoundCx<'cx, 'tcx> { // see the extensive comment in projection_must_outlive let ty = self.tcx.mk_projection(projection_ty.item_def_id, projection_ty.substs); - let recursive_bound = self.recursive_bound(ty.into()); + let recursive_bound = self.recursive_bound(ty.into(), visited); VerifyBound::AnyBound(env_bounds.chain(trait_bounds).collect()).or(recursive_bound) } - fn recursive_bound(&self, parent: GenericArg<'tcx>) -> VerifyBound<'tcx> { + fn recursive_bound( + &self, + parent: GenericArg<'tcx>, + visited: &mut MiniSet<GenericArg<'tcx>>, + ) -> VerifyBound<'tcx> { let mut bounds = parent - .walk_shallow() + .walk_shallow(visited) .filter_map(|child| match child.unpack() { - GenericArgKind::Type(ty) => Some(self.type_bound(ty)), + GenericArgKind::Type(ty) => Some(self.type_bound(ty, visited)), GenericArgKind::Lifetime(lt) => { // Ignore late-bound regions. if !lt.is_late_bound() { Some(VerifyBound::OutlivedBy(lt)) } else { None } } - GenericArgKind::Const(_) => Some(self.recursive_bound(child)), + GenericArgKind::Const(_) => Some(self.recursive_bound(child, visited)), }) .filter(|bound| { // Remove bounds that must hold, since they are not interesting. diff --git a/compiler/rustc_middle/Cargo.toml b/compiler/rustc_middle/Cargo.toml index a5a860a38b3..1d84ddad7f5 100644 --- a/compiler/rustc_middle/Cargo.toml +++ b/compiler/rustc_middle/Cargo.toml @@ -28,5 +28,6 @@ rustc_ast = { path = "../rustc_ast" } rustc_span = { path = "../rustc_span" } chalk-ir = "0.21.0" smallvec = { version = "1.0", features = ["union", "may_dangle"] } +arrayvec = { version = "0.5.1", default-features = false } measureme = "0.7.1" rustc_session = { path = "../rustc_session" } diff --git a/compiler/rustc_middle/src/ty/outlives.rs b/compiler/rustc_middle/src/ty/outlives.rs index 783f116a87d..01649f44c88 100644 --- a/compiler/rustc_middle/src/ty/outlives.rs +++ b/compiler/rustc_middle/src/ty/outlives.rs @@ -3,6 +3,7 @@ // RFC for reference. use crate::ty::subst::{GenericArg, GenericArgKind}; +use crate::ty::walk::MiniSet; use crate::ty::{self, Ty, TyCtxt, TypeFoldable}; use smallvec::SmallVec; @@ -50,12 +51,18 @@ impl<'tcx> TyCtxt<'tcx> { /// Push onto `out` all the things that must outlive `'a` for the condition /// `ty0: 'a` to hold. Note that `ty0` must be a **fully resolved type**. pub fn push_outlives_components(self, ty0: Ty<'tcx>, out: &mut SmallVec<[Component<'tcx>; 4]>) { - compute_components(self, ty0, out); + let mut visited = MiniSet::new(); + compute_components(self, ty0, out, &mut visited); debug!("components({:?}) = {:?}", ty0, out); } } -fn compute_components(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, out: &mut SmallVec<[Component<'tcx>; 4]>) { +fn compute_components( + tcx: TyCtxt<'tcx>, + ty: Ty<'tcx>, + out: &mut SmallVec<[Component<'tcx>; 4]>, + visited: &mut MiniSet<GenericArg<'tcx>>, +) { // Descend through the types, looking for the various "base" // components and collecting them into `out`. This is not written // with `collect()` because of the need to sometimes skip subtrees @@ -73,11 +80,11 @@ fn compute_components(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, out: &mut SmallVec<[Compo for child in substs { match child.unpack() { GenericArgKind::Type(ty) => { - compute_components(tcx, ty, out); + compute_components(tcx, ty, out, visited); } GenericArgKind::Lifetime(_) => {} GenericArgKind::Const(_) => { - compute_components_recursive(tcx, child, out); + compute_components_recursive(tcx, child, out, visited); } } } @@ -85,19 +92,19 @@ fn compute_components(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, out: &mut SmallVec<[Compo ty::Array(element, _) => { // Don't look into the len const as it doesn't affect regions - compute_components(tcx, element, out); + compute_components(tcx, element, out, visited); } ty::Closure(_, ref substs) => { for upvar_ty in substs.as_closure().upvar_tys() { - compute_components(tcx, upvar_ty, out); + compute_components(tcx, upvar_ty, out, visited); } } ty::Generator(_, ref substs, _) => { // Same as the closure case for upvar_ty in substs.as_generator().upvar_tys() { - compute_components(tcx, upvar_ty, out); + compute_components(tcx, upvar_ty, out, visited); } // We ignore regions in the generator interior as we don't @@ -135,7 +142,8 @@ fn compute_components(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, out: &mut SmallVec<[Compo // OutlivesProjectionComponents. Continue walking // through and constrain Pi. let mut subcomponents = smallvec![]; - compute_components_recursive(tcx, ty.into(), &mut subcomponents); + let mut subvisited = MiniSet::new(); + compute_components_recursive(tcx, ty.into(), &mut subcomponents, &mut subvisited); out.push(Component::EscapingProjection(subcomponents.into_iter().collect())); } } @@ -177,7 +185,7 @@ fn compute_components(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, out: &mut SmallVec<[Compo // the "bound regions list". In our representation, no such // list is maintained explicitly, because bound regions // themselves can be readily identified. - compute_components_recursive(tcx, ty.into(), out); + compute_components_recursive(tcx, ty.into(), out, visited); } } } @@ -186,11 +194,12 @@ fn compute_components_recursive( tcx: TyCtxt<'tcx>, parent: GenericArg<'tcx>, out: &mut SmallVec<[Component<'tcx>; 4]>, + visited: &mut MiniSet<GenericArg<'tcx>>, ) { - for child in parent.walk_shallow() { + for child in parent.walk_shallow(visited) { match child.unpack() { GenericArgKind::Type(ty) => { - compute_components(tcx, ty, out); + compute_components(tcx, ty, out, visited); } GenericArgKind::Lifetime(lt) => { // Ignore late-bound regions. @@ -199,7 +208,7 @@ fn compute_components_recursive( } } GenericArgKind::Const(_) => { - compute_components_recursive(tcx, child, out); + compute_components_recursive(tcx, child, out, visited); } } } diff --git a/compiler/rustc_middle/src/ty/walk.rs b/compiler/rustc_middle/src/ty/walk.rs index 4f55517c6f4..7afa6e6cc05 100644 --- a/compiler/rustc_middle/src/ty/walk.rs +++ b/compiler/rustc_middle/src/ty/walk.rs @@ -3,7 +3,50 @@ use crate::ty; use crate::ty::subst::{GenericArg, GenericArgKind}; +use arrayvec::ArrayVec; +use rustc_data_structures::fx::FxHashSet; use smallvec::{self, SmallVec}; +use std::hash::Hash; + +/// Small-storage-optimized implementation of a set +/// made specifically for walking type tree. +/// +/// Stores elements in a small array up to a certain length +/// and switches to `HashSet` when that length is exceeded. +pub enum MiniSet<T> { + Array(ArrayVec<[T; 8]>), + Set(FxHashSet<T>), +} + +impl<T: Eq + Hash + Copy> MiniSet<T> { + /// Creates an empty `MiniSet`. + pub fn new() -> Self { + MiniSet::Array(ArrayVec::new()) + } + + /// Adds a value to the set. + /// + /// If the set did not have this value present, true is returned. + /// + /// If the set did have this value present, false is returned. + pub fn insert(&mut self, elem: T) -> bool { + match self { + MiniSet::Array(array) => { + if array.iter().any(|e| *e == elem) { + false + } else { + if array.try_push(elem).is_err() { + let mut set: FxHashSet<T> = array.iter().copied().collect(); + set.insert(elem); + *self = MiniSet::Set(set); + } + true + } + } + MiniSet::Set(set) => set.insert(elem), + } + } +} // The TypeWalker's stack is hot enough that it's worth going to some effort to // avoid heap allocations. @@ -12,11 +55,20 @@ type TypeWalkerStack<'tcx> = SmallVec<[GenericArg<'tcx>; 8]>; pub struct TypeWalker<'tcx> { stack: TypeWalkerStack<'tcx>, last_subtree: usize, + visited: MiniSet<GenericArg<'tcx>>, } +/// An iterator for walking the type tree. +/// +/// It's very easy to produce a deeply +/// nested type tree with a lot of +/// identical subtrees. In order to work efficiently +/// in this situation walker only visits each type once. +/// It maintains a set of visited types and +/// skips any types that are already there. impl<'tcx> TypeWalker<'tcx> { - pub fn new(root: GenericArg<'tcx>) -> TypeWalker<'tcx> { - TypeWalker { stack: smallvec![root], last_subtree: 1 } + pub fn new(root: GenericArg<'tcx>) -> Self { + Self { stack: smallvec![root], last_subtree: 1, visited: MiniSet::new() } } /// Skips the subtree corresponding to the last type @@ -41,11 +93,15 @@ impl<'tcx> Iterator for TypeWalker<'tcx> { fn next(&mut self) -> Option<GenericArg<'tcx>> { debug!("next(): stack={:?}", self.stack); - let next = self.stack.pop()?; - self.last_subtree = self.stack.len(); - push_inner(&mut self.stack, next); - debug!("next: stack={:?}", self.stack); - Some(next) + loop { + let next = self.stack.pop()?; + self.last_subtree = self.stack.len(); + if self.visited.insert(next) { + push_inner(&mut self.stack, next); + debug!("next: stack={:?}", self.stack); + return Some(next); + } + } } } @@ -67,9 +123,17 @@ impl GenericArg<'tcx> { /// Iterator that walks the immediate children of `self`. Hence /// `Foo<Bar<i32>, u32>` yields the sequence `[Bar<i32>, u32]` /// (but not `i32`, like `walk`). - pub fn walk_shallow(self) -> impl Iterator<Item = GenericArg<'tcx>> { + /// + /// Iterator only walks items once. + /// It accepts visited set, updates it with all visited types + /// and skips any types that are already there. + pub fn walk_shallow( + self, + visited: &mut MiniSet<GenericArg<'tcx>>, + ) -> impl Iterator<Item = GenericArg<'tcx>> { let mut stack = SmallVec::new(); push_inner(&mut stack, self); + stack.retain(|a| visited.insert(*a)); stack.into_iter() } } diff --git a/src/test/ui/closures/issue-72408-nested-closures-exponential.rs b/src/test/ui/closures/issue-72408-nested-closures-exponential.rs new file mode 100644 index 00000000000..2d6ba936572 --- /dev/null +++ b/src/test/ui/closures/issue-72408-nested-closures-exponential.rs @@ -0,0 +1,59 @@ +// build-pass + +// Closures include captured types twice in a type tree. +// +// Wrapping one closure with another leads to doubling +// the amount of types in the type tree. +// +// This test ensures that rust can handle +// deeply nested type trees with a lot +// of duplicated subtrees. + +fn dup(f: impl Fn(i32) -> i32) -> impl Fn(i32) -> i32 { + move |a| f(a * 2) +} + +fn main() { + let f = |a| a; + + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + + // Compiler dies around here if it tries + // to walk the tree exhaustively. + + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + let f = dup(f); + + println!("Type size was at least {}", f(1)); +} diff --git a/src/test/ui/issues/issue-22638.rs b/src/test/ui/issues/issue-22638.rs index 72c16fddb4b..89137538425 100644 --- a/src/test/ui/issues/issue-22638.rs +++ b/src/test/ui/issues/issue-22638.rs @@ -51,9 +51,9 @@ struct D (Box<A>); impl D { pub fn matches<F: Fn()>(&self, f: &F) { - //~^ ERROR reached the type-length limit while instantiating `D::matches::<[closure let &D(ref a) = self; a.matches(f) + //~^ ERROR reached the recursion limit while instantiating `A::matches::<[closure } } diff --git a/src/test/ui/issues/issue-22638.stderr b/src/test/ui/issues/issue-22638.stderr index b0df46b11fa..c4255b95b70 100644 --- a/src/test/ui/issues/issue-22638.stderr +++ b/src/test/ui/issues/issue-22638.stderr @@ -1,10 +1,14 @@ -error: reached the type-length limit while instantiating `D::matches::$CLOSURE` - --> $DIR/issue-22638.rs:53:5 +error: reached the recursion limit while instantiating `A::matches::$CLOSURE` + --> $DIR/issue-22638.rs:55:9 + | +LL | a.matches(f) + | ^^^^^^^^^^^^ + | +note: `A::matches` defined here + --> $DIR/issue-22638.rs:14:5 | LL | pub fn matches<F: Fn()>(&self, f: &F) { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: consider adding a `#![type_length_limit="30408681"]` attribute to your crate error: aborting due to previous error diff --git a/src/test/ui/type_length_limit.rs b/src/test/ui/type_length_limit.rs index 1f1c8ad9626..921cded5037 100644 --- a/src/test/ui/type_length_limit.rs +++ b/src/test/ui/type_length_limit.rs @@ -4,7 +4,7 @@ // Test that the type length limit can be changed. #![allow(dead_code)] -#![type_length_limit="256"] +#![type_length_limit="4"] macro_rules! link { ($id:ident, $t:ty) => { diff --git a/src/test/ui/type_length_limit.stderr b/src/test/ui/type_length_limit.stderr index f12f259d2f6..e8fddfd4ece 100644 --- a/src/test/ui/type_length_limit.stderr +++ b/src/test/ui/type_length_limit.stderr @@ -4,7 +4,7 @@ error: reached the type-length limit while instantiating `std::mem::drop::<Optio LL | pub fn drop<T>(_x: T) {} | ^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: consider adding a `#![type_length_limit="1094"]` attribute to your crate + = note: consider adding a `#![type_length_limit="8"]` attribute to your crate error: aborting due to previous error |
