Optimize try_evaluate_obligations - #160479
Conversation
|
Some changes occurred to the core trait solver cc @rust-lang/initiative-trait-system-refactor |
fb5d400 to
c0a9583
Compare
|
@bors try @rust-timer queue |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…try> Optimize `try_evaluate_obligations`
This comment has been minimized.
This comment has been minimized.
|
Finished benchmarking commit (443ade9): comparison URL. Overall result: ✅ improvements - no action neededBenchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf. @bors rollup=never rustc-perf Instruction countOur most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.
Max RSS (memory usage)Results (primary -0.3%, secondary -0.9%)A less reliable metric. May be of interest, but not used to determine the overall result above.
CyclesResults (primary -1.1%, secondary -2.3%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeThis perf run didn't have relevant results for this metric. Bootstrap: 489.838s -> 490.758s (0.19%) |
|
The perf results on CI are a delight:
|
|
r? me |
| self.inspect_evaluated_obligation(infcx, &obligation, &result); | ||
| // Common case: no inspector, still stalled; keep the obligation. This path is | ||
| // extremely hot in some cases; there can be thousands of pending obligations. | ||
| if !has_inspector |
There was a problem hiding this comment.
can throw that check out. There's no use in reinspecting a stalled goal as it hasn't changed since the last time :> inspectors only exist for external tools 😁
There was a problem hiding this comment.
Ok. I have added a second commit for this.
|
I have three follow-up changes that will improve these cases more. |
| return true; | ||
| } | ||
|
|
||
| let result = delegate.evaluate_root_goal( |
There was a problem hiding this comment.
note: the first thing evaluate_root_goal does is to check again goal_remains_stalled.
There was a problem hiding this comment.
Yes, there is some repeated work: rerunning_stalled_goal_may_make_progress can be called twice. It's idempotent and the wasted work doesn't matter. Here's some Cachegrind output:
. // Common case: no inspector, still stalled; keep the obligation. This path is
. // extremely hot in some cases; there can be thousands of pending obligations.
114,082,625 (0.8%) if !has_inspector
228,165,250 (1.6%) && let Some(stalled_on) = opt_stalled_on
. && let Some(certainty) = delegate.goal_remains_stalled(stalled_on)
. && matches!(certainty, Certainty::Maybe(_))
. {
. return true;
. }
.
622,248 (0.0%) let result = delegate.evaluate_root_goal(
. obligation.as_goal(),
155,562 (0.0%) obligation.cause.span,
. opt_stalled_on.take(),
. );
The common case is more than 100x hotter than what follows. It would be possible to refactor evaluate_root_goal to avoid this wasted work, but evaluate_root_goal has four call sites and they would all need some changes and I don't think it's worthwhile.
This function is very sub-optimal, perf-wise: it takes `self.obligations.pending` (with `mem::take`) and iterates over the elements, checking each one. But most of the time no progress is made and all the obligations get pushed back onto `self.obligations.pending`. This drain + reconstruct approach is very expensive, mostly because the new `pending` vec is built by pushing one element at a time, which requires repeated reallocations. And this vec can have thousands of elements in it, in extreme cases. Also, `obligation` and `stalled_on` get passed by value to `evaluate_root_goal` (`obligation` as `goal`), which then usually passes the values back in the `GoalEvaluation` which is immediately deconstructed. This is a lot of wasted value moves. This commit optimizes things in two ways. - It prioritizes the hot path. This involves checking in advance if there is an inspector (usually not) and adding `goal_remains_stalled` which takes `stalled_on` by reference. This hot path avoids all the value moves and `GoalEvaluation` construction/deconstruction and gets to the very common "nothing needed to be done" outcome as quickly as possible. - It uses `retain_mut` to update `self.obligations.pending`. This requires some adjustments (e.g. handling recursion via the `overflowed` flag with some cleanup code after the `retain_mut` call, and cloning obligations in the error cases).
Don't call the inspector on the hot path when nothing has changed. This is a visible behaviour change, but as lcnr said: "There's no use in reinspecting a stalled goal as it hasn't changed since the last time" and "inspectors only exist for external tools".
95c355e to
4b0dd6f
Compare
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
|
I updated the code. I added the suggested comments. The other suggestions above were about inefficiencies on the cold path and I don't think anything needs changing there. Good to go? |
|
I love it, lgtm! @bors r+ |
| // some cases; there can be thousands of pending obligations. | ||
| if let Some(stalled_on) = opt_stalled_on | ||
| && let Some(certainty) = delegate.goal_remains_stalled(stalled_on) | ||
| && matches!(certainty, Certainty::Maybe(_)) |
There was a problem hiding this comment.
we should change stalled_on to contain a MaybeCause instead of a Certainty. It is always Certainty::Maybe
This comment has been minimized.
This comment has been minimized.
…donszelmann Optimize `try_evaluate_obligations` This function is very sub-optimal, perf-wise: it takes `self.obligations.pending` (with `mem::take`) and iterates over the elements, checking each one. But most of the time no progress is made and all the obligations get pushed back onto `self.obligations.pending`. This drain + reconstruct approach is very expensive, mostly because the new `pending` vec is built by pushing one element at a time, which requires repeated reallocations. And this vec can have thousands of elements in it, in extreme cases. Also, `obligation` and `stalled_on` get passed by value to `evaluate_root_goal` (`obligation` as `goal`), which then usually passes the values back in the `GoalEvaluation` which is immediately deconstructed. This is a lot of wasted value moves. This commit optimizes things in two ways. - It prioritizes the hot path. This involves checking in advance if there is an inspector (usually not) and adding `goal_remains_stalled` which takes `stalled_on` by reference. This hot path avoids all the value moves and `GoalEvaluation` construction/deconstruction and gets to the very common "nothing needed to be done" outcome as quickly as possible. - It uses `retain_mut` to update `self.obligations.pending`. This requires some adjustments (e.g. handling recursion via the `overflowed` flag with some cleanup code after the `retain_mut` call, and cloning obligations in the error cases). r? @lcnr cc @jdonszelmann @WaffleLapkin
|
hmmm |

View all comments
This function is very sub-optimal, perf-wise: it takes
self.obligations.pending(withmem::take) and iterates over the elements, checking each one. But most of the time no progress is made and all the obligations get pushed back ontoself.obligations.pending. This drain + reconstruct approach is very expensive, mostly because the newpendingvec is built by pushing one element at a time, which requires repeated reallocations. And this vec can have thousands of elements in it, in extreme cases.Also,
obligationandstalled_onget passed by value toevaluate_root_goal(obligationasgoal), which then usually passes the values back in theGoalEvaluationwhich is immediately deconstructed. This is a lot of wasted value moves.This commit optimizes things in two ways.
It prioritizes the hot path. This involves checking in advance if there is an inspector (usually not) and adding
goal_remains_stalledwhich takesstalled_onby reference. This hot path avoids all the value moves andGoalEvaluationconstruction/deconstruction and gets to the very common "nothing needed to be done" outcome as quickly as possible.It uses
retain_mutto updateself.obligations.pending. This requires some adjustments (e.g. handling recursion via theoverflowedflag with some cleanup code after theretain_mutcall, and cloning obligations in the error cases).r? @lcnr
cc @jdonszelmann @WaffleLapkin