rust-lang / rust-lang/rust

Trait solver re-derives `Send` proofs per impl when an outlives where-bound is in scope

Open
#157,595 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-trait-system I-compiletime T-types
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.