Trait solver re-derives `Send` proofs per impl when an outlives where-bound is in scope
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
One trait, M impls, each wrapping the same Shared struct (150 fields of Arc<Mutex<Vec<…>>> nested 10 deep); every method's future borrows &self. The two files differ only in how the method's lifetimes are spelled:
// outlives_*.rs — the shape #[async_trait] emits for every method
fn check<'life0, 'at>(&'life0 self, x: &'at [u8])
-> Pin<Box<dyn Future<Output = u64> + Send + 'at>>
where Self: 'at, 'life0: 'at;
// unified_*.rs — same borrows under one lifetime, no outlives clause
fn check<'a>(&'a self, x: &'a [u8]) -> Pin<Box<dyn Future<Output = u64> + Send + 'a>>;
$ git clone https://github.com/ashi009/region-cache-repro && cd region-cache-repro
$ rustc -V
rustc 1.96.0 (ac68faa20 2026-05-25)
$ time rustc --edition 2021 --crate-type=lib --emit=metadata -o /tmp/r.rmeta outlives_150.rs
real 0m3.326s
$ time rustc --edition 2021 --crate-type=lib --emit=metadata -o /tmp/r.rmeta outlives_300.rs
real 0m6.627s
$ time rustc --edition 2021 --crate-type=lib --emit=metadata -o /tmp/r.rmeta unified_150.rs
real 0m0.134s
$ time rustc --edition 2021 --crate-type=lib --emit=metadata -o /tmp/r.rmeta unified_300.rs
real 0m0.180s
$ rustc -V
rustc 1.98.0-nightly (8954863c8 2026-06-05)
$ time rustc --edition 2021 --crate-type=lib --emit=metadata -o /tmp/r.rmeta outlives_300.rs
real 0m6.612s
$ time rustc --edition 2021 --crate-type=lib --emit=metadata -o /tmp/r.rmeta unified_300.rs
real 0m0.188s
$ time rustc --edition 2021 --crate-type=lib --emit=metadata -Znext-solver=globally -o /tmp/r.rmeta outlives_300.rs
real 0m14.539s
$ time rustc --edition 2021 --crate-type=lib --emit=metadata -Znext-solver=globally -o /tmp/r.rmeta unified_300.rs
real 0m0.273s
97% of the outlives time is evaluate_obligation, re-deriving the same Shared: Send proof once per impl. The outlives bound puts a region in caller_bounds. The query canonicalizes the ParamEnv, so that region comes back as an infer var. can_use_global_caches then bails on param_env.has_infer(), and the proof never reaches tcx.evaluation_cache. #[async_trait] emits that bound on every method, so large async codebases pay this per method × impl.
Same diagnosis and fix as #92044 (validated on #87012), closed unmerged over selection soundness (impl<T: 'static> bounds participate in selection); it reproduces unchanged today.
(-Znext-solver=globally not removing the gap is unexpected; cause not investigated.)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Clone region-cache-repro and reproduce the outlives_150.rs, outlives_300.rs, and unified_*.rs timings with rustc. Start at evaluate_obligation and the ParamEnv/canonicalization path, then compare the diagnosis and proposed fix in #92044 and validation in #87012. Done means addressing the repeated Send proof without reopening the selection-soundness concern.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100