`MIR_borrow_checking` scales with captured-type nesting when an async block is boxed at a distinct return region (the `#[async_trait]` shape)
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.2k
- PR merge metrics
- PR metrics pending
Description
Note: I worked with Claude to write the issue description.
Summary
Boxing an async block from a method that borrows &self and returns the future at
a distinct return region — the shape #[async_trait] desugars to — makes
MIR_borrow_checking grow with the nesting depth of the captured type's
region-bearing components. Returning the future at the borrow's own region
instead is flat in nesting and >10× faster on a full build.
Reproduction
Standalone, raw rustc, no cargo / no third-party deps: https://github.com/MashPlant/async-trait-compile-test
The leaf wraps a large nested type from a dependency crate and exposes the same
boxed Send future three ways:
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
// method1 — boxed at the natural borrow lifetime (FAST)
fn f<'life0>(&'life0 self) -> BoxFuture<'life0, ()> { Box::pin(async move { let _ = self; }) }
// method2 — the shape `#[async_trait]` generates: distinct `'async_trait` (SLOW)
fn f<'life0, 'async_trait>(&'life0 self) -> BoxFuture<'async_trait, ()>
where 'life0: 'async_trait, Self: 'async_trait { Box::pin(async move { let _ = self; }) }
// method3 — RPITIT async fn + a generic blanket impl that boxes the opaque future (FAST)
Timings (full pipeline, -Ztime-passes, DEPTH=12/8191 structs, NEST=16)
method total borrowck type_check mono_walk metadata other
method2 1.544 1.503 0.002 0 0.001 0.038
method3 0.118 0.001 0.048 0.027 0.028 0.014
method1 0.075 0.035 0.002 0 0.001 0.037
The entire method2 difference is MIR_borrow_checking (1.50s vs ~0). The ratio
tracks per-leaf nesting (NEST), not the type's size. (drop_in_place::<Big> is
instantiated upstream and shared, so this is a real full-build cost, not a
metadata artifact — see repo.)
Root cause (as far as -Ztime-passes shows; details in repo)
Region/outlives checking. method1's Self: 'life0 is the implied bound that
well-formedness of &'life0 self already provides (discharged in O(1), no type
walk); method2's Self: 'async_trait is not that implied bound, so borrowck
decomposes Self and re-walks every region-bearing component. Remove any one of
{distinct return region, async generator, region-bearing captured type} and the
gap disappears. Region-free leaves (Vec<Box<u8>>) don't trigger it → it is
outlives checking, not drop-checking.
Related issues (believed distinct — different compiler phase)
- #126440 —
MIR_borrow_checkingvery slow, open,E-needs-mcve. Same phase;
this repro may be a usable MCVE. - dtolnay/async-trait#174 (closed), #87012 (open) — same async-trait family but
blamed onevaluate_obligation(trait solving), not borrowck.
Meta
rustc 1.95.0 (59807616e 2026-04-14), host aarch64-unknown-linux-gnu, LLVM 22.1.2
-Ztime-passes enabled on stable via RUSTC_BOOTSTRAP=1.
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
Start with the linked standalone raw-rustc reproduction and compare method1 and method2 using -Ztime-passes. Trace MIR_borrow_checking's region/outlives checking, especially the distinct return region and region-bearing captured type. Done means removing the nesting-dependent cost while preserving the reported async behavior and validating the timing difference.
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
- Mostly clear
- Newbie friendliness
- 38/100