rust-lang / rust-lang/rust

`MIR_borrow_checking` scales with captured-type nesting when an async block is boxed at a distinct return region (the `#[async_trait]` shape)

Open
#158,224 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs-triage
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_checking very 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 on evaluate_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

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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.