Skip to content

Optimize try_evaluate_obligations - #160479

Merged
rust-bors[bot] merged 2 commits into
rust-lang:mainfrom
nnethercote:opt-try_evaluate_obligations
Aug 7, 2026
Merged

Optimize try_evaluate_obligations#160479
rust-bors[bot] merged 2 commits into
rust-lang:mainfrom
nnethercote:opt-try_evaluate_obligations

Conversation

@nnethercote

@nnethercote nnethercote commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

View all comments

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

@rustbot

rustbot commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Some changes occurred to the core trait solver

cc @rust-lang/initiative-trait-system-refactor

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver) labels Aug 4, 2026
@nnethercote
nnethercote force-pushed the opt-try_evaluate_obligations branch from fb5d400 to c0a9583 Compare August 4, 2026 02:34
@nnethercote

Copy link
Copy Markdown
Contributor Author

On my Linux box this reduced the wall-time for a check full build of nacl-0.5.3 and ijson-0.1.6 by ~50%, and of nvml-wrapper-sys-0.9.1 by ~25%.

Local instruction count results for all the new-solver benchmarks (including those three, which I have added locally but aren't on CI):

image

@nnethercote

Copy link
Copy Markdown
Contributor Author

@bors try @rust-timer queue

@rust-timer

This comment has been minimized.

@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Aug 4, 2026
@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Aug 4, 2026
…try>

Optimize `try_evaluate_obligations`
@rust-bors

rust-bors Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

☀️ Try build successful (CI)
Build commit: 443ade9 (443ade9a155ac75fdadee81c20ae7a9b8576e119)
Base parent: c9ff496 (c9ff496891c278ad660bc0ab85c1f0b72059464a)

@rust-timer

This comment has been minimized.

@rust-timer

Copy link
Copy Markdown
Collaborator

Finished benchmarking commit (443ade9): comparison URL.

Overall result: ✅ improvements - no action needed

Benchmarking 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
@rustbot label: -S-waiting-on-perf -perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-1.7% [-5.1%, -0.2%] 19
All ❌✅ (primary) - - 0

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.

mean range count
Regressions ❌
(primary)
4.4% [4.4%, 4.4%] 1
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
-0.6% [-1.2%, -0.4%] 16
Improvements ✅
(secondary)
-0.9% [-2.6%, -0.4%] 26
All ❌✅ (primary) -0.3% [-1.2%, 4.4%] 17

Cycles

Results (primary -1.1%, secondary -2.3%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
0.5% [0.4%, 0.7%] 4
Regressions ❌
(secondary)
0.9% [0.5%, 1.3%] 5
Improvements ✅
(primary)
-1.8% [-7.0%, -0.5%] 10
Improvements ✅
(secondary)
-3.1% [-11.1%, -0.4%] 19
All ❌✅ (primary) -1.1% [-7.0%, 0.7%] 14

Binary size

This perf run didn't have relevant results for this metric.

Bootstrap: 489.838s -> 490.758s (0.19%)
Artifact size: 390.28 MiB -> 390.21 MiB (-0.02%)

@rustbot rustbot removed the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Aug 4, 2026
@nnethercote

Copy link
Copy Markdown
Contributor Author

The perf results on CI are a delight:

  • icount reductions are a bit bigger than I saw locally across all new-solver benchmarks, e.g. -5.12% for wg-grammar on CI vs. -3.96% locally (for a check full build).
  • cycles and wall-time both show a 10% reduction for a check full build of wg-grammar, and 1-6% across a majority of the other new-solver benchmarks.

@jdonszelmann

Copy link
Copy Markdown
Contributor

r? me

@rustbot rustbot assigned jdonszelmann and unassigned lcnr Aug 4, 2026
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

@lcnr lcnr Aug 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 😁

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I have added a second commit for this.

@nnethercote

Copy link
Copy Markdown
Contributor Author

I have three follow-up changes that will improve these cases more.

return true;
}

let result = delegate.evaluate_root_goal(

@jdonszelmann jdonszelmann Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: the first thing evaluate_root_goal does is to check again goal_remains_stalled.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread compiler/rustc_trait_selection/src/solve/fulfill.rs
Comment thread compiler/rustc_trait_selection/src/solve/fulfill.rs
Comment thread compiler/rustc_trait_selection/src/solve/fulfill.rs
@nnethercote
nnethercote force-pushed the opt-try_evaluate_obligations branch from 95c355e to 4b0dd6f Compare August 5, 2026 13:01
@rustbot

This comment has been minimized.

@nnethercote

Copy link
Copy Markdown
Contributor Author

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?

@jdonszelmann

Copy link
Copy Markdown
Contributor

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(_))

@lcnr lcnr Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should change stalled_on to contain a MaybeCause instead of a Certainty. It is always Certainty::Maybe

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a follow-up?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Aug 6, 2026
…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
@rust-bors rust-bors Bot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. labels Aug 6, 2026
@rust-bors

rust-bors Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

💔 Test for 84aa78d failed: CI

@JonathanBrouwer

Copy link
Copy Markdown
Contributor

hmmm
@bors retry

@rust-bors rust-bors Bot added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. labels Aug 6, 2026
@rust-bors

This comment has been minimized.

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".
@nnethercote
nnethercote force-pushed the opt-try_evaluate_obligations branch from 4b0dd6f to 7089725 Compare August 7, 2026 09:35
@rustbot

rustbot commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

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.

@nnethercote

Copy link
Copy Markdown
Contributor Author

@bors r=jdonszelmann

@rust-bors

rust-bors Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📌 Commit 7089725 has been approved by jdonszelmann

It is now in the queue for this repository.

@rust-bors rust-bors Bot added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Aug 7, 2026
@rust-bors

This comment has been minimized.

@rust-bors rust-bors Bot added merged-by-bors This PR was explicitly merged by bors. and removed S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. labels Aug 7, 2026
@rust-bors

rust-bors Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

☀️ Test successful - CI
Approved by: jdonszelmann
Duration: 3h 4m 19s
Pushing 65bcac4 to main...

@rust-bors
rust-bors Bot merged commit 65bcac4 into rust-lang:main Aug 7, 2026
14 checks passed
@rustbot rustbot added this to the 1.99.0 milestone Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor
What is this? This is an experimental post-merge analysis report that shows differences in test outcomes between the merged PR and its parent PR.

Comparing ae45457 (parent) -> 65bcac4 (this PR)

Test differences

Show 2 test diffs

2 doctest diffs were found. These are ignored, as they are noisy.

Test dashboard

Run

cargo run --manifest-path src/ci/citool/Cargo.toml -- \
    test-dashboard 65bcac45b3d8a8b2126e5cc844cf6fff5795d32a --output-dir test-dashboard

And then open test-dashboard/index.html in your browser to see an overview of all executed tests.

Job duration changes

  1. x86_64-msvc-2: 1h 43m -> 2h 27m (+41.9%)
  2. x86_64-msvc-ext1: 1h 40m -> 2h 17m (+36.5%)
  3. dist-x86_64-netbsd: 1h 28m -> 56m 32s (-36.0%)
  4. i686-gnu-nopt-1: 1h 36m -> 2h 8m (+34.3%)
  5. x86_64-gnu-llvm-22-3: 1h 44m -> 1h 9m (-33.5%)
  6. dist-powerpc-linux: 1h 9m -> 1h 30m (+31.0%)
  7. dist-various-1: 36m 16s -> 44m 41s (+23.2%)
  8. x86_64-gnu-debug: 1h 32m -> 1h 53m (+22.9%)
  9. dist-ohos-armv7: 1h 11m -> 55m 39s (-22.7%)
  10. dist-x86_64-linux-alt: 2h 15m -> 1h 45m (-22.5%)
How to interpret the job duration changes?

Job durations can vary a lot, based on the actual runner instance
that executed the job, system noise, invalidated caches, etc. The table above is provided
mostly for t-infra members, for simpler debugging of potential CI slow-downs.

@rust-timer

Copy link
Copy Markdown
Collaborator

Finished benchmarking commit (65bcac4): comparison URL.

Overall result: ✅ improvements - no action needed

@rustbot label: -perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-1.6% [-5.1%, -0.2%] 20
All ❌✅ (primary) - - 0

Max RSS (memory usage)

Results (secondary -1.1%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
0.7% [0.4%, 1.1%] 4
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-2.2% [-11.9%, -0.4%] 7
All ❌✅ (primary) - - 0

Cycles

Results (primary 2.1%, secondary -2.0%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
2.1% [2.1%, 2.1%] 1
Regressions ❌
(secondary)
1.0% [0.4%, 1.9%] 7
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-3.5% [-10.4%, -0.4%] 14
All ❌✅ (primary) 2.1% [2.1%, 2.1%] 1

Binary size

This perf run didn't have relevant results for this metric.

Bootstrap: 461.168s -> 458.664s (-0.54%)
Artifact size: 398.56 MiB -> 398.60 MiB (0.01%)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merged-by-bors This PR was explicitly merged by bors. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants