Skip to content

Perf: fulfillment wake by stalled var#22911

Draft
tilladam wants to merge 2 commits into
rust-lang:masterfrom
tilladam:perf/fulfillment-wake-by-stalled-var
Draft

Perf: fulfillment wake by stalled var#22911
tilladam wants to merge 2 commits into
rust-lang:masterfrom
tilladam:perf/fulfillment-wake-by-stalled-var

Conversation

@tilladam

@tilladam tilladam commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Problem: try_evaluate_obligations drained/re-touched the whole pending set per call; body inference calls it ~O(n) times -> O(n²) per body. This hits large generated bodies particularly as they exist in both rust-analyzer and slint (and I'm sure elsewhere also).

UndoLogs::in_snapshot for InferCtxtUndoLogs deliberately returns true so ena reports all mutations (its gates skip push outside snapshots). Real undo entries stay gated internally, an inherent in_snapshot provides the real check. This could/should later be replaced by a small upstream ena API (an "always observe" hook), if you guys agree this is the right approach.

Impact measurements (macOS M5-series, release):

synthetic 8k-stmt body (one fn) 23.6s (down from 86.8s after #22910) -> 0.38s

RA self-analysis, total inference: 24.4s -> 14.0s (across this and #22910)
Slint generate_enums_pyi body: 3.64s -> 0.49s
Slint workspace inference: 40.6s → 27.4s

Correctness: full self-analysis differential (unknown types, type mismatches, pattern variants, MIR failed bodies, failed const evals) identical before/after, peak RSS unchanged, 1018/0 hir-ty tests, clippy clean.

This prepares better parallelization because making those bodies near-linear:

  • shortens that serial critical path
  • reduces workload imbalance between threads
  • raises the maximum useful speedup from future body-level parallelization

LLM help was used in the analysis, profiling and implementation of this change.

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jul 25, 2026
@tilladam tilladam changed the title Perf/fulfillment wake by stalled var Perf: fulfillment wake by stalled var Jul 25, 2026
…tions

`FulfillmentCtxt::try_evaluate_obligations` re-processes every pending
ambiguous obligation on every call. For obligations resolved by
`compute_goal_fast_path`, the `Certainty::Maybe` branch discarded the
obligation's `GoalStalledOn` (re-registering with `None`), which meant
every subsequent call fully re-ran the fast path's predicate resolution
(`shallow_resolve` + `is_trivially_wf`) instead of being able to skip it,
even though the real solver's own internal stall-check exists precisely
to avoid this.

Reconstruct a `GoalStalledOn` for these obligations by walking the
goal's predicate for the inference variables it depends on
(`fast_path_stalled_on`), and check it up front with the same primitives
the solver's internal fast path already uses (`is_still_stalled`), so an
unchanged obligation is skipped before touching the solver at all.

This is a constant-factor fix, not asymptotic (the per-call scan over
all pending obligations remains O(n)). Measured: prefill 7.79s -> 4.35s
(1.79x), full self-analysis inference 24.4s -> ~17-18s (~1.4x), up to
3.9x per-obligation on a synthetic worst case. Full-repo differential
(unknown type / type mismatches / pattern unknown type / pattern type
mismatches / mir failed bodies / failed const evals) identical
before/after; `cargo test -p hir-ty` 1015/0 both ways; clippy clean.
@tilladam
tilladam force-pushed the perf/fulfillment-wake-by-stalled-var branch from 31cd27f to 086b356 Compare July 25, 2026 12:41
…d of rescanning

`FulfillmentCtxt::try_evaluate_obligations` drained and re-touched the
*entire* pending set on every call. Body inference calls it O(n) times
(from `select_obligations_where_possible` via structural resolution,
method calls, operators, ...), making single-body inference O(n²) in the
number of expressions. Large macro-generated bodies dominate
whole-workspace inference: `intern::symbol::symbols::prefill` (~13k
exprs) alone was ~35% of all inference time on self-analysis.

This replaces the rescan with an event-driven index:

- `InferCtxtUndoLogs` now keeps an append-only `changed_vars` log of
  every inference-variable slot `ena` writes to. `ena` gates its
  undo-log `push` calls on `UndoLogs::in_snapshot()`, which would lose
  every mutation made while no snapshot is open, so that trait method
  deliberately answers `true` and the real undo entries are gated on
  `num_open_snapshots` internally instead; an inherent `in_snapshot`
  shadows the trait method for code that wants the truth. Draining the
  opaque-type storage (the one mutation path that bypasses the undo
  machinery) records an explicit event.
- Each `FulfillmentCtxt` reads the log through its own cursor
  (non-destructively, so nested `ObligationCtxt`s sharing the infcx
  cannot steal the body-scoped context's wake signals) and wakes exactly
  the obligations parked on a changed variable via a slab + watch-key
  index. Obligations are only parked when their `GoalStalledOn` was just
  verified intact — which also guarantees the tracked variables are
  unification roots, the identity `ena` fires events on. Stall sets that
  cannot be watched (`Fresh*` vars) stay on a periodic recheck path that
  re-runs once per call and after rounds that made real progress, which
  is also what makes the fixpoint loop terminate.
- A debug-builds-only sweep asserts after every wake pass that no parked
  obligation's stall condition was invalidated without a wake (the
  failure mode of this design is silently wrong inference results, so it
  must fail loudly in tests), and that the slab's live counter is in
  sync.

Measured (macOS, release): synthetic one-body benchmark N=1000/2000/
4000/8000 statements goes from 0.48s/1.61s/6.0s/23.6s to 0.13s/0.15s/
0.23s/0.38s (per-doubling exponent ~1.9 -> ~0.7); `analysis-stats .
--only prefill` inference 4.5s -> 2.6s; whole-workspace self-analysis
inference 17.6s -> 14.0s; peak RSS unchanged. Differential check on full
self-analysis: `unknown type`, `type mismatches`, pattern variants,
`mir failed bodies`, `failed const evals` all identical to before.

Three snapshots in `tests::regression` (`recursive_vars`,
`recursive_vars_2`, `infer_std_crash_5`) were re-blessed: they encode
the exact number of `&'?` layers accumulated before the recursion limit
trips on pathological recursive-unknown code (annotated in-test as an
acknowledged artifact), and the reworked loop reaches the limit with a
±1 round count. The final types are the same `{unknown}`-based fallback.
@tilladam
tilladam force-pushed the perf/fulfillment-wake-by-stalled-var branch from 086b356 to 76cc8ea Compare July 25, 2026 13:01
@ChayimFriedman2

ChayimFriedman2 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

This code is copied from rustc and we will absolutely not change it from what rustc does. If rustc was already changed and we haven't followed, please point at the rustc code. If not, the only way for this to be accepted is to first suggest this to rust-lang/rust.

@tilladam

Copy link
Copy Markdown
Contributor Author

This code is copied from rustc and we will absolutely not change it from what rustc does. If rustc was already changed and we haven't followed, please point at the rustc code. If not, the only way for this to be accepted is to first suggest this to rust-lang/rust.

Fair enough. It's still a problem in rustc and will hit them once the new solver stabilizes. I'll attempt to find someone to point the quadratic behavior out to on zulip or via an issue then.

@tilladam
tilladam marked this pull request as draft July 25, 2026 19:15
@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jul 25, 2026
@tilladam

Copy link
Copy Markdown
Contributor Author

Filed against rustc here: rust-lang/rust#159933.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants