Skip to content

Fix quadratic MIR blowup for large vec![] expressions with Drop-implementing elements in async functions#154720

Open
jakubadamw wants to merge 4 commits intorust-lang:mainfrom
jakubadamw:issue-115327
Open

Fix quadratic MIR blowup for large vec![] expressions with Drop-implementing elements in async functions#154720
jakubadamw wants to merge 4 commits intorust-lang:mainfrom
jakubadamw:issue-115327

Conversation

@jakubadamw
Copy link
Copy Markdown
Contributor

@jakubadamw jakubadamw commented Apr 2, 2026

In #115327, constructing a large Vec<String> inside an async fn used to cause a quadratic blowup in the number of MIR basic blocks, making compilation very slow (minutes for tens of thousands items). Example:

fn main() {
    async_function();
}

async fn async_function() -> Vec<String> {
    vec![
        "string".to_string(), 
        // repeat the above line 10,000 times
    ]
}

The root cause was that StorageDead instructions on the unwind path prevented tail-sharing of drop chains in coroutines. Each element in the vec initialiser got its own unique unwind chain because of distinct StorageDead locals, leading to ~n² basic blocks.

The fix checks whether the coroutine body actually contains any yield points. If there are no yields (e.g. an async fn with no .await), no locals can be live across a yield, so StorageDead on the unwind path can safely be skipped – which restores the tail-sharing and brings MIR size back to linear.

Now, async functions with no await aren’t perhaps necessarily that common, but are sort of inevitable in the case of traits defining a method as async with implementations deciding not to have any yield points for whatever reason.

This PR also adds support for a //@ timeout: N directive to compiletest, so that individual tests can set their own warning timeout (the regression test uses a 10-second timeout to catch any future regressions). This is perhaps controversial or should be separated out into another issue & PR and discussed independently, but I felt like this could be the best way to test this performance improvement. In the future, the compiletest machinery could be made so that the defined timeouts actually lead to an immediate failure rather than merely a warning.

Accordingly, the PR includes a run-make test that compiles a generated async fn returning a vec!["string".to_string(), …] with 10,000 elements, much like the original example from the issue.

I'm not familiar with this part of the compiler so I’d be happy to hear if there's a better way to detect the yield-free case – checking the THIR for ExprKind::Yield seemed like the most straightforward approach, but perhaps there’s a better way?

I guess the PR would deserve a perf run to make sure this doesn’t impact the ”happy path” overly.

Closes #115327.

Add a `//@ timeout: <seconds>` directive that lets individual tests
override the default compilation timeout. This is useful for regression
tests that guard against quadratic or otherwise slow compilation, where
the default timeout is too generous to catch a regression.
For coroutines, `StorageDead` operations were unconditionally added to
the unwind path. For the included test, each array element's unwind chain
includes unique `StorageDead` locals, preventing tail-sharing between chains.
This produces O(n^2) MIR basic blocks for large aggregate initialisers
(e.g. sum(1 to n) = ~50M blocks for n = 10,000).

`StorageDead` on the unwind path is needed for correctness: without it,
the coroutine drop handler may re-drop locals already dropped during
unwinding. However, this can only happen when the coroutine body contains
yield points, because locals can only span a yield if one exists.

Fix: scan the THIR at construction time for `Yield` expressions.
When none are found (for example, in an async function with no `.await`),
set `storage_dead_on_unwind = false` on all drops, skipping `StorageDead`
on the unwind path, which makes the MIR block count linear rather than quadratic.
…arge `Vec<String>` construction

Add a run-make test that generates and compiles an `async` function
returning a `vec!["string".to_string(), …]` with 10,000 elements.
Without the preceding fix, this example would produce tens of millions
of MIR basic blocks and take minutes to compile.
With the fix in place, it compiles in a few seconds.

The timeout specified in the test makes it so it would fail before the fix applied.
@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Apr 2, 2026

compiletest directives have been modified. Please add or update docs for the
new or modified directive in src/doc/rustc-dev-guide/.

Some changes occurred in src/tools/compiletest

cc @jieyouxu

@rustbot rustbot added A-compiletest Area: The compiletest test runner A-run-make Area: port run-make Makefiles to rmake.rs A-testsuite Area: The testsuite used to check the correctness of rustc S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Apr 2, 2026
@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Apr 2, 2026

r? @jdonszelmann

rustbot has assigned @jdonszelmann.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: compiler, mir
  • compiler, mir expanded to 69 candidates
  • Random selection from 12 candidates

@rust-log-analyzer

This comment has been minimized.

@jakubadamw jakubadamw marked this pull request as draft April 2, 2026 17:32
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Apr 2, 2026
@jakubadamw jakubadamw marked this pull request as ready for review April 2, 2026 17:40
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Apr 2, 2026
@matthiaskrgr
Copy link
Copy Markdown
Member

@bors try @rust-timer queue

@rust-timer
Copy link
Copy Markdown
Collaborator

Awaiting bors try build completion.

@rustbot label: +S-waiting-on-perf

@rust-bors
Copy link
Copy Markdown
Contributor

rust-bors bot commented Apr 2, 2026

⌛ Trying commit 55c8a49 with merge ab08649

To cancel the try build, run the command @bors try cancel.

Workflow: https://github.com/rust-lang/rust/actions/runs/23917572818

rust-bors bot pushed a commit that referenced this pull request Apr 2, 2026
Fix quadratic MIR blowup for large `vec![]` expressions with `Drop`-implementing elements in async functions
@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-compiletest Area: The compiletest test runner A-run-make Area: port run-make Makefiles to rmake.rs A-testsuite Area: The testsuite used to check the correctness of rustc S-waiting-on-perf Status: Waiting on a perf run to be completed. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slow compilation of async fn that creates a vector with many Strings created from string literals, even with no .await

6 participants