Perf: fulfillment wake by stalled var#22911
Conversation
…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.
31cd27f to
086b356
Compare
…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.
086b356 to
76cc8ea
Compare
|
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. |
|
Filed against rustc here: rust-lang/rust#159933. |
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:
LLM help was used in the analysis, profiling and implementation of this change.