Skip to content

Commit d6cffe4

Browse files
committed
Auto merge of #89831 - Aaron1011:project-caching-speedup, r=jackh726
Re-introduce concept of projection cache 'completion' Instead of clearing out the cache entirely, we store the intermediate evaluation result into the cache entry. This accomplishes several things: * We avoid the performance hit associated with re-evaluating the sub-obligations * We avoid causing issues with incremental compilation, since the final evaluation result is always the same * We avoid affecting other uses of the same `InferCtxt` which might care about 'side effects' from processing the sub-obligations (e,g. region constraints). Only code that is specifically aware of the new 'complete' code is affected
2 parents df2f45c + 40ef1d3 commit d6cffe4

File tree

5 files changed

+138
-6
lines changed

5 files changed

+138
-6
lines changed

compiler/rustc_infer/src/traits/project.rs

+69-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use rustc_data_structures::{
1010
};
1111
use rustc_middle::ty::{self, Ty};
1212

13-
pub use rustc_middle::traits::Reveal;
13+
pub use rustc_middle::traits::{EvaluationResult, Reveal};
1414

1515
pub(crate) type UndoLog<'tcx> =
1616
snapshot_map::UndoLog<ProjectionCacheKey<'tcx>, ProjectionCacheEntry<'tcx>>;
@@ -92,7 +92,42 @@ pub enum ProjectionCacheEntry<'tcx> {
9292
Ambiguous,
9393
Recur,
9494
Error,
95-
NormalizedTy(NormalizedTy<'tcx>),
95+
NormalizedTy {
96+
ty: NormalizedTy<'tcx>,
97+
/// If we were able to successfully evaluate the
98+
/// corresponding cache entry key during predicate
99+
/// evaluation, then this field stores the final
100+
/// result obtained from evaluating all of the projection
101+
/// sub-obligations. During evaluation, we will skip
102+
/// evaluating the cached sub-obligations in `ty`
103+
/// if this field is set. Evaluation only
104+
/// cares about the final result, so we don't
105+
/// care about any region constraint side-effects
106+
/// produced by evaluating the sub-boligations.
107+
///
108+
/// Additionally, we will clear out the sub-obligations
109+
/// entirely if we ever evaluate the cache entry (along
110+
/// with all its sub obligations) to `EvaluatedToOk`.
111+
/// This affects all users of the cache, not just evaluation.
112+
/// Since a result of `EvaluatedToOk` means that there were
113+
/// no region obligations that need to be tracked, it's
114+
/// fine to forget about the sub-obligations - they
115+
/// don't provide any additional information. However,
116+
/// we do *not* discard any obligations when we see
117+
/// `EvaluatedToOkModuloRegions` - we don't know
118+
/// which sub-obligations may introduce region constraints,
119+
/// so we keep them all to be safe.
120+
///
121+
/// When we are not performing evaluation
122+
/// (e.g. in `FulfillmentContext`), we ignore this field,
123+
/// and always re-process the cached sub-obligations
124+
/// (which may have been cleared out - see the above
125+
/// paragraph).
126+
/// This ensures that we do not lose any regions
127+
/// constraints that arise from processing the
128+
/// sub-obligations.
129+
complete: Option<EvaluationResult>,
130+
},
96131
}
97132

98133
impl<'tcx> ProjectionCacheStorage<'tcx> {
@@ -149,10 +184,41 @@ impl<'tcx> ProjectionCache<'_, 'tcx> {
149184
debug!("Not overwriting Recur");
150185
return;
151186
}
152-
let fresh_key = map.insert(key, ProjectionCacheEntry::NormalizedTy(value));
187+
let fresh_key =
188+
map.insert(key, ProjectionCacheEntry::NormalizedTy { ty: value, complete: None });
153189
assert!(!fresh_key, "never started projecting `{:?}`", key);
154190
}
155191

192+
/// Mark the relevant projection cache key as having its derived obligations
193+
/// complete, so they won't have to be re-computed (this is OK to do in a
194+
/// snapshot - if the snapshot is rolled back, the obligations will be
195+
/// marked as incomplete again).
196+
pub fn complete(&mut self, key: ProjectionCacheKey<'tcx>, result: EvaluationResult) {
197+
let mut map = self.map();
198+
match map.get(&key) {
199+
Some(&ProjectionCacheEntry::NormalizedTy { ref ty, complete: _ }) => {
200+
info!("ProjectionCacheEntry::complete({:?}) - completing {:?}", key, ty);
201+
let mut ty = ty.clone();
202+
if result == EvaluationResult::EvaluatedToOk {
203+
ty.obligations = vec![];
204+
}
205+
map.insert(key, ProjectionCacheEntry::NormalizedTy { ty, complete: Some(result) });
206+
}
207+
ref value => {
208+
// Type inference could "strand behind" old cache entries. Leave
209+
// them alone for now.
210+
info!("ProjectionCacheEntry::complete({:?}) - ignoring {:?}", key, value);
211+
}
212+
};
213+
}
214+
215+
pub fn is_complete(&mut self, key: ProjectionCacheKey<'tcx>) -> Option<EvaluationResult> {
216+
self.map().get(&key).and_then(|res| match res {
217+
ProjectionCacheEntry::NormalizedTy { ty: _, complete } => *complete,
218+
_ => None,
219+
})
220+
}
221+
156222
/// Indicates that trying to normalize `key` resulted in
157223
/// ambiguity. No point in trying it again then until we gain more
158224
/// type information (in which case, the "fully resolved" key will

compiler/rustc_trait_selection/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#![feature(drain_filter)]
1717
#![feature(derive_default_enum)]
1818
#![feature(hash_drain_filter)]
19+
#![feature(label_break_value)]
1920
#![feature(let_else)]
2021
#![feature(never_type)]
2122
#![feature(crate_visibility_modifier)]

compiler/rustc_trait_selection/src/traits/fulfill.rs

+17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use rustc_data_structures::obligation_forest::ProcessResult;
44
use rustc_data_structures::obligation_forest::{Error, ForestObligation, Outcome};
55
use rustc_data_structures::obligation_forest::{ObligationForest, ObligationProcessor};
66
use rustc_errors::ErrorReported;
7+
use rustc_infer::traits::ProjectionCacheKey;
78
use rustc_infer::traits::{SelectionError, TraitEngine, TraitEngineExt as _, TraitObligation};
89
use rustc_middle::mir::interpret::ErrorHandled;
910
use rustc_middle::thir::abstract_const::NotConstEvaluatable;
@@ -20,12 +21,14 @@ use super::wf;
2021
use super::CodeAmbiguity;
2122
use super::CodeProjectionError;
2223
use super::CodeSelectionError;
24+
use super::EvaluationResult;
2325
use super::Unimplemented;
2426
use super::{FulfillmentError, FulfillmentErrorCode};
2527
use super::{ObligationCause, PredicateObligation};
2628

2729
use crate::traits::error_reporting::InferCtxtExt as _;
2830
use crate::traits::project::PolyProjectionObligation;
31+
use crate::traits::project::ProjectionCacheKeyExt as _;
2932
use crate::traits::query::evaluate_obligation::InferCtxtExt as _;
3033

3134
impl<'tcx> ForestObligation for PendingPredicateObligation<'tcx> {
@@ -709,6 +712,20 @@ impl<'a, 'b, 'tcx> FulfillProcessor<'a, 'b, 'tcx> {
709712
// no type variables present, can use evaluation for better caching.
710713
// FIXME: consider caching errors too.
711714
if self.selcx.infcx().predicate_must_hold_considering_regions(obligation) {
715+
if let Some(key) = ProjectionCacheKey::from_poly_projection_predicate(
716+
&mut self.selcx,
717+
project_obligation.predicate,
718+
) {
719+
// If `predicate_must_hold_considering_regions` succeeds, then we've
720+
// evaluated all sub-obligations. We can therefore mark the 'root'
721+
// obligation as complete, and skip evaluating sub-obligations.
722+
self.selcx
723+
.infcx()
724+
.inner
725+
.borrow_mut()
726+
.projection_cache()
727+
.complete(key, EvaluationResult::EvaluatedToOk);
728+
}
712729
return ProcessResult::Changed(vec![]);
713730
} else {
714731
tracing::debug!("Does NOT hold: {:?}", obligation);

compiler/rustc_trait_selection/src/traits/project.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -889,7 +889,7 @@ fn opt_normalize_projection_type<'a, 'b, 'tcx>(
889889
debug!("recur cache");
890890
return Err(InProgress);
891891
}
892-
Err(ProjectionCacheEntry::NormalizedTy(ty)) => {
892+
Err(ProjectionCacheEntry::NormalizedTy { ty, complete: _ }) => {
893893
// This is the hottest path in this function.
894894
//
895895
// If we find the value in the cache, then return it along

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

+50-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ use super::{ObligationCause, PredicateObligation, TraitObligation};
2525

2626
use crate::infer::{InferCtxt, InferOk, TypeFreshener};
2727
use crate::traits::error_reporting::InferCtxtExt;
28+
use crate::traits::project::ProjectionCacheKeyExt;
29+
use crate::traits::ProjectionCacheKey;
2830
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
2931
use rustc_data_structures::stack::ensure_sufficient_stack;
3032
use rustc_data_structures::sync::Lrc;
@@ -550,8 +552,54 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
550552
let project_obligation = obligation.with(data);
551553
match project::poly_project_and_unify_type(self, &project_obligation) {
552554
Ok(Ok(Some(mut subobligations))) => {
553-
self.add_depth(subobligations.iter_mut(), obligation.recursion_depth);
554-
self.evaluate_predicates_recursively(previous_stack, subobligations)
555+
'compute_res: {
556+
// If we've previously marked this projection as 'complete', thne
557+
// use the final cached result (either `EvaluatedToOk` or
558+
// `EvaluatedToOkModuloRegions`), and skip re-evaluating the
559+
// sub-obligations.
560+
if let Some(key) =
561+
ProjectionCacheKey::from_poly_projection_predicate(self, data)
562+
{
563+
if let Some(cached_res) = self
564+
.infcx
565+
.inner
566+
.borrow_mut()
567+
.projection_cache()
568+
.is_complete(key)
569+
{
570+
break 'compute_res Ok(cached_res);
571+
}
572+
}
573+
574+
self.add_depth(
575+
subobligations.iter_mut(),
576+
obligation.recursion_depth,
577+
);
578+
let res = self.evaluate_predicates_recursively(
579+
previous_stack,
580+
subobligations,
581+
);
582+
if let Ok(res) = res {
583+
if res == EvaluatedToOk || res == EvaluatedToOkModuloRegions {
584+
if let Some(key) =
585+
ProjectionCacheKey::from_poly_projection_predicate(
586+
self, data,
587+
)
588+
{
589+
// If the result is something that we can cache, then mark this
590+
// entry as 'complete'. This will allow us to skip evaluating the
591+
// suboligations at all the next time we evaluate the projection
592+
// predicate.
593+
self.infcx
594+
.inner
595+
.borrow_mut()
596+
.projection_cache()
597+
.complete(key, res);
598+
}
599+
}
600+
}
601+
res
602+
}
555603
}
556604
Ok(Ok(None)) => Ok(EvaluatedToAmbig),
557605
Ok(Err(project::InProgress)) => Ok(EvaluatedToRecur),

0 commit comments

Comments
 (0)