Skip to content

Commit 4075f43

Browse files
committed
dedup obligations in opt_normalize_projection_type
1 parent 489296d commit 4075f43

File tree

5 files changed

+156
-59
lines changed

5 files changed

+156
-59
lines changed

compiler/rustc_infer/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#![feature(box_patterns)]
1818
#![feature(derive_default_enum)]
1919
#![feature(extend_one)]
20+
#![feature(hash_raw_entry)]
2021
#![feature(let_else)]
2122
#![feature(never_type)]
2223
#![feature(control_flow_enum)]

compiler/rustc_infer/src/traits/mod.rs

+63
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ mod project;
88
mod structural_impls;
99
pub mod util;
1010

11+
use rustc_data_structures::fx::FxHashMap;
1112
use rustc_hir as hir;
1213
use rustc_middle::ty::error::{ExpectedFound, TypeError};
1314
use rustc_middle::ty::{self, Const, Ty, TyCtxt};
1415
use rustc_span::Span;
16+
use std::borrow::{Borrow, Cow};
17+
use std::collections::hash_map::RawEntryMut;
18+
use std::hash::Hash;
1519

1620
pub use self::FulfillmentErrorCode::*;
1721
pub use self::ImplSource::*;
@@ -105,6 +109,65 @@ pub enum FulfillmentErrorCode<'tcx> {
105109
CodeAmbiguity,
106110
}
107111

112+
pub struct ObligationsDedup<'a, 'tcx, T> {
113+
obligations: &'a mut Vec<Obligation<'tcx, T>>,
114+
}
115+
116+
impl<'a, 'tcx, T: 'tcx> ObligationsDedup<'a, 'tcx, T>
117+
where
118+
T: Clone + Hash + Eq,
119+
{
120+
pub fn from_vec(vec: &'a mut Vec<Obligation<'tcx, T>>) -> Self {
121+
ObligationsDedup { obligations: vec }
122+
}
123+
124+
pub fn extend<'b>(&mut self, iter: impl ExactSizeIterator<Item = Cow<'b, Obligation<'tcx, T>>>)
125+
where
126+
'tcx: 'b,
127+
{
128+
// obligation tracing has shown that initial batches added to an empty vec do not
129+
// contain any duplicates, so there's no need to attempt deduplication
130+
if self.obligations.is_empty() {
131+
*self.obligations = iter.into_iter().map(Cow::into_owned).collect();
132+
return;
133+
}
134+
135+
let initial_size = self.obligations.len();
136+
let iter = iter.into_iter();
137+
let expected_new = iter.len();
138+
let combined_size = initial_size + expected_new;
139+
140+
if combined_size <= 16 || combined_size < initial_size.next_power_of_two() {
141+
// small case/not crossing a power of two. don't bother with dedup
142+
self.obligations.extend(iter.map(Cow::into_owned));
143+
} else {
144+
// crossing power of two threshold. this would incur a vec growth anyway if we didn't do
145+
// anything. piggyback a dedup on that
146+
let obligations = std::mem::take(self.obligations);
147+
148+
let mut seen = FxHashMap::default();
149+
seen.reserve(initial_size);
150+
151+
*self.obligations = obligations
152+
.into_iter()
153+
.map(Cow::Owned)
154+
.chain(iter)
155+
.filter_map(|obligation| {
156+
match seen.raw_entry_mut().from_key(obligation.borrow()) {
157+
RawEntryMut::Occupied(..) => {
158+
return None;
159+
}
160+
RawEntryMut::Vacant(vacant) => {
161+
vacant.insert(obligation.clone().into_owned(), ());
162+
}
163+
}
164+
Some(obligation.into_owned())
165+
})
166+
.collect();
167+
}
168+
}
169+
}
170+
108171
impl<'tcx, O> Obligation<'tcx, O> {
109172
pub fn new(
110173
cause: ObligationCause<'tcx>,

compiler/rustc_infer/src/traits/project.rs

+14
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,20 @@ impl<'tcx> ProjectionCache<'_, 'tcx> {
173173
Ok(())
174174
}
175175

176+
pub fn try_start_borrowed<'a, T>(
177+
&'a mut self,
178+
key: ProjectionCacheKey<'tcx>,
179+
with: impl FnOnce(&'_ ProjectionCacheEntry<'tcx>) -> T + 'a,
180+
) -> Option<T> {
181+
let mut map = self.map();
182+
if let Some(entry) = map.get(&key) {
183+
return Some(with(entry));
184+
}
185+
186+
map.insert(key, ProjectionCacheEntry::InProgress);
187+
None
188+
}
189+
176190
/// Indicates that `key` was normalized to `value`.
177191
pub fn insert_ty(&mut self, key: ProjectionCacheKey<'tcx>, value: NormalizedTy<'tcx>) {
178192
debug!(

compiler/rustc_trait_selection/src/traits/project.rs

+74-59
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ use rustc_middle::ty::subst::Subst;
3030
use rustc_middle::ty::{self, ToPredicate, Ty, TyCtxt};
3131
use rustc_span::symbol::sym;
3232

33+
use std::borrow::Cow;
3334
use std::collections::BTreeMap;
3435

36+
use crate::traits::ObligationsDedup;
3537
pub use rustc_middle::traits::Reveal;
3638

3739
pub type PolyProjectionObligation<'tcx> = Obligation<'tcx, ty::PolyProjectionPredicate<'tcx>>;
@@ -839,6 +841,8 @@ fn opt_normalize_projection_type<'a, 'b, 'tcx>(
839841
// mode, which could lead to using incorrect cache results.
840842
let use_cache = !selcx.is_intercrate();
841843

844+
let mut obligations = ObligationsDedup::from_vec(obligations);
845+
842846
let projection_ty = infcx.resolve_vars_if_possible(projection_ty);
843847
let cache_key = ProjectionCacheKey::new(projection_ty);
844848

@@ -850,65 +854,76 @@ fn opt_normalize_projection_type<'a, 'b, 'tcx>(
850854
// bounds. It might be the case that we want two distinct caches,
851855
// or else another kind of cache entry.
852856

853-
let cache_result = if use_cache {
854-
infcx.inner.borrow_mut().projection_cache().try_start(cache_key)
855-
} else {
856-
Ok(())
857-
};
858-
match cache_result {
859-
Ok(()) => debug!("no cache"),
860-
Err(ProjectionCacheEntry::Ambiguous) => {
861-
// If we found ambiguity the last time, that means we will continue
862-
// to do so until some type in the key changes (and we know it
863-
// hasn't, because we just fully resolved it).
864-
debug!("found cache entry: ambiguous");
865-
return Ok(None);
866-
}
867-
Err(ProjectionCacheEntry::InProgress) => {
868-
// Under lazy normalization, this can arise when
869-
// bootstrapping. That is, imagine an environment with a
870-
// where-clause like `A::B == u32`. Now, if we are asked
871-
// to normalize `A::B`, we will want to check the
872-
// where-clauses in scope. So we will try to unify `A::B`
873-
// with `A::B`, which can trigger a recursive
874-
// normalization.
875-
876-
debug!("found cache entry: in-progress");
877-
878-
// Cache that normalizing this projection resulted in a cycle. This
879-
// should ensure that, unless this happens within a snapshot that's
880-
// rolled back, fulfillment or evaluation will notice the cycle.
857+
if use_cache {
858+
let result =
859+
infcx.inner.borrow_mut().projection_cache().try_start_borrowed(cache_key, |cached| {
860+
match cached {
861+
ProjectionCacheEntry::NormalizedTy { ty, complete: _ } => {
862+
// This is the hottest path in this function.
863+
//
864+
// If we find the value in the cache, then return it along
865+
// with the obligations that went along with it. Note
866+
// that, when using a fulfillment context, these
867+
// obligations could in principle be ignored: they have
868+
// already been registered when the cache entry was
869+
// created (and hence the new ones will quickly be
870+
// discarded as duplicated). But when doing trait
871+
// evaluation this is not the case, and dropping the trait
872+
// evaluations can causes ICEs (e.g., #43132).
873+
debug!(?ty, "found normalized ty");
874+
obligations.extend(ty.obligations.iter().map(Cow::Borrowed));
875+
Ok(Some(ty.value))
876+
}
877+
cached @ _ => Err(cached.clone()),
878+
}
879+
});
881880

882-
if use_cache {
883-
infcx.inner.borrow_mut().projection_cache().recur(cache_key);
881+
match result {
882+
Some(Ok(ret)) => return Ok(ret),
883+
Some(Err(cached)) => {
884+
return match cached {
885+
ProjectionCacheEntry::Ambiguous => {
886+
// If we found ambiguity the last time, that means we will continue
887+
// to do so until some type in the key changes (and we know it
888+
// hasn't, because we just fully resolved it).
889+
debug!("found cache entry: ambiguous");
890+
Ok(None)
891+
}
892+
ProjectionCacheEntry::InProgress => {
893+
// Under lazy normalization, this can arise when
894+
// bootstrapping. That is, imagine an environment with a
895+
// where-clause like `A::B == u32`. Now, if we are asked
896+
// to normalize `A::B`, we will want to check the
897+
// where-clauses in scope. So we will try to unify `A::B`
898+
// with `A::B`, which can trigger a recursive
899+
// normalization.
900+
901+
debug!("found cache entry: in-progress");
902+
903+
// Cache that normalizing this projection resulted in a cycle. This
904+
// should ensure that, unless this happens within a snapshot that's
905+
// rolled back, fulfillment or evaluation will notice the cycle.
906+
907+
if use_cache {
908+
infcx.inner.borrow_mut().projection_cache().recur(cache_key);
909+
}
910+
Err(InProgress)
911+
}
912+
ProjectionCacheEntry::Recur => {
913+
debug!("recur cache");
914+
Err(InProgress)
915+
}
916+
ProjectionCacheEntry::Error => {
917+
debug!("opt_normalize_projection_type: found error");
918+
let result =
919+
normalize_to_error(selcx, param_env, projection_ty, cause, depth);
920+
obligations.extend(result.obligations.into_iter().map(Cow::Owned));
921+
Ok(Some(result.value))
922+
}
923+
_ => unreachable!("unexpected variant"),
924+
};
884925
}
885-
return Err(InProgress);
886-
}
887-
Err(ProjectionCacheEntry::Recur) => {
888-
debug!("recur cache");
889-
return Err(InProgress);
890-
}
891-
Err(ProjectionCacheEntry::NormalizedTy { ty, complete: _ }) => {
892-
// This is the hottest path in this function.
893-
//
894-
// If we find the value in the cache, then return it along
895-
// with the obligations that went along with it. Note
896-
// that, when using a fulfillment context, these
897-
// obligations could in principle be ignored: they have
898-
// already been registered when the cache entry was
899-
// created (and hence the new ones will quickly be
900-
// discarded as duplicated). But when doing trait
901-
// evaluation this is not the case, and dropping the trait
902-
// evaluations can causes ICEs (e.g., #43132).
903-
debug!(?ty, "found normalized ty");
904-
obligations.extend(ty.obligations);
905-
return Ok(Some(ty.value));
906-
}
907-
Err(ProjectionCacheEntry::Error) => {
908-
debug!("opt_normalize_projection_type: found error");
909-
let result = normalize_to_error(selcx, param_env, projection_ty, cause, depth);
910-
obligations.extend(result.obligations);
911-
return Ok(Some(result.value));
926+
_ => {}
912927
}
913928
}
914929

@@ -955,7 +970,7 @@ fn opt_normalize_projection_type<'a, 'b, 'tcx>(
955970
if use_cache {
956971
infcx.inner.borrow_mut().projection_cache().insert_ty(cache_key, result.clone());
957972
}
958-
obligations.extend(result.obligations);
973+
obligations.extend(result.obligations.into_iter().map(Cow::Owned));
959974
Ok(Some(result.value))
960975
}
961976
Ok(ProjectedTy::NoProgress(projected_ty)) => {
@@ -985,7 +1000,7 @@ fn opt_normalize_projection_type<'a, 'b, 'tcx>(
9851000
infcx.inner.borrow_mut().projection_cache().error(cache_key);
9861001
}
9871002
let result = normalize_to_error(selcx, param_env, projection_ty, cause, depth);
988-
obligations.extend(result.obligations);
1003+
obligations.extend(result.obligations.into_iter().map(Cow::Owned));
9891004
Ok(Some(result.value))
9901005
}
9911006
}

compiler/rustc_trait_selection/src/traits/select/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -2337,6 +2337,10 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
23372337
// This code is hot enough that it's worth avoiding the allocation
23382338
// required for the FxHashSet when possible. Special-casing lengths 0,
23392339
// 1 and 2 covers roughly 75-80% of the cases.
2340+
//
2341+
// Ideally we would perform deduplication incrementally in the predicates
2342+
// loop above to prevent excessive Vec growth but that would require
2343+
// a Vec::range_retain or similar method.
23402344
if obligations.len() <= 1 {
23412345
// No possibility of duplicates.
23422346
} else if obligations.len() == 2 {

0 commit comments

Comments
 (0)