Skip to content

Commit ab08649

Browse files
committed
Auto merge of #154720 - jakubadamw:issue-115327, r=<try>
Fix quadratic MIR blowup for large `vec![]` expressions with `Drop`-implementing elements in async functions
2 parents e6b64a2 + 55c8a49 commit ab08649

File tree

8 files changed

+126
-50
lines changed

8 files changed

+126
-50
lines changed

compiler/rustc_mir_build/src/builder/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use rustc_infer::infer::{InferCtxt, TyCtxtInferExt};
3636
use rustc_middle::hir::place::PlaceBase as HirPlaceBase;
3737
use rustc_middle::middle::region;
3838
use rustc_middle::mir::*;
39-
use rustc_middle::thir::{self, ExprId, LocalVarId, Param, ParamId, PatKind, Thir};
39+
use rustc_middle::thir::{self, ExprId, ExprKind, LocalVarId, Param, ParamId, PatKind, Thir};
4040
use rustc_middle::ty::{self, ScalarInt, Ty, TyCtxt, TypeVisitableExt, TypingMode};
4141
use rustc_middle::{bug, span_bug};
4242
use rustc_session::lint;
@@ -178,6 +178,11 @@ struct Builder<'a, 'tcx> {
178178
arg_count: usize,
179179
coroutine: Option<Box<CoroutineInfo<'tcx>>>,
180180

181+
/// Whether this coroutine's body contains any yield points (`.await`).
182+
/// When false, no locals can be live across a yield, so `StorageDead`
183+
/// can be omitted from the unwind path — see `DropData::storage_dead_on_unwind`.
184+
coroutine_has_yields: bool,
185+
181186
/// The current set of scopes, updated as we traverse;
182187
/// see the `scope` module for more details.
183188
scopes: scope::Scopes<'tcx>,
@@ -777,6 +782,8 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
777782
cfg: CFG { basic_blocks: IndexVec::new() },
778783
fn_span: span,
779784
arg_count,
785+
coroutine_has_yields: coroutine.is_some()
786+
&& thir.exprs.iter().any(|expr| matches!(expr.kind, ExprKind::Yield { .. })),
780787
coroutine,
781788
scopes: scope::Scopes::new(),
782789
block_context: BlockContext::new(),

compiler/rustc_mir_build/src/builder/scope.rs

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,20 @@ struct DropData {
159159

160160
/// Whether this is a value Drop or a StorageDead.
161161
kind: DropKind,
162+
163+
/// Whether this drop's `StorageDead` should be emitted on the unwind path.
164+
/// For coroutines with yield points, `StorageDead` on the unwind path is
165+
/// needed to prevent the coroutine drop handler from re-dropping locals
166+
/// already dropped during unwinding. However, emitting them for every drop
167+
/// in a coroutine causes O(n^2) MIR basic blocks for large aggregate
168+
/// initialisers (each element's unwind chain becomes unique due to distinct
169+
/// `StorageDead` locals, preventing tail-sharing).
170+
///
171+
/// To avoid the quadratic blowup, this flag is only set when the coroutine
172+
/// body contains yield points. A coroutine without yields (e.g. an async fn
173+
/// with no `.await`) cannot have locals live across a yield, so the drop
174+
/// handler issue does not arise.
175+
storage_dead_on_unwind: bool,
162176
}
163177

164178
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -263,12 +277,13 @@ impl Scope {
263277
/// * polluting the cleanup MIR with StorageDead creates
264278
/// landing pads even though there's no actual destructors
265279
/// * freeing up stack space has no effect during unwinding
266-
/// Note that for coroutines we do emit StorageDeads, for the
267-
/// use of optimizations in the MIR coroutine transform.
280+
/// Note that for coroutines we may also emit `StorageDead` on
281+
/// unwind for drops scheduled after a yield point — see
282+
/// `DropData::storage_dead_on_unwind`.
268283
fn needs_cleanup(&self) -> bool {
269284
self.drops.iter().any(|drop| match drop.kind {
270285
DropKind::Value | DropKind::ForLint => true,
271-
DropKind::Storage => false,
286+
DropKind::Storage => drop.storage_dead_on_unwind,
272287
})
273288
}
274289

@@ -296,8 +311,12 @@ impl DropTree {
296311
// represents the block in the tree that should be jumped to once all
297312
// of the required drops have been performed.
298313
let fake_source_info = SourceInfo::outermost(DUMMY_SP);
299-
let fake_data =
300-
DropData { source_info: fake_source_info, local: Local::MAX, kind: DropKind::Storage };
314+
let fake_data = DropData {
315+
source_info: fake_source_info,
316+
local: Local::MAX,
317+
kind: DropKind::Storage,
318+
storage_dead_on_unwind: false,
319+
};
301320
let drop_nodes = IndexVec::from_raw(vec![DropNode { data: fake_data, next: DropIdx::MAX }]);
302321
Self { drop_nodes, entry_points: Vec::new(), existing_drops_map: FxHashMap::default() }
303322
}
@@ -1111,7 +1130,12 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
11111130
return None;
11121131
}
11131132

1114-
Some(DropData { source_info, local, kind: DropKind::Value })
1133+
Some(DropData {
1134+
source_info,
1135+
local,
1136+
kind: DropKind::Value,
1137+
storage_dead_on_unwind: false,
1138+
})
11151139
}
11161140
Operand::Constant(_) | Operand::RuntimeChecks(_) => None,
11171141
})
@@ -1239,7 +1263,6 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
12391263
block,
12401264
unwind_to,
12411265
dropline_to,
1242-
is_coroutine && needs_cleanup,
12431266
self.arg_count,
12441267
|v: Local| Self::is_async_drop_impl(self.tcx, &self.local_decls, typing_env, v),
12451268
)
@@ -1474,10 +1497,23 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
14741497
// path, we only need to invalidate the cache for drops that happen on
14751498
// the unwind or coroutine drop paths. This means that for
14761499
// non-coroutines we don't need to invalidate caches for `DropKind::Storage`.
1477-
let invalidate_caches = needs_drop || self.coroutine.is_some();
1500+
//
1501+
// For coroutines, `StorageDead` on the unwind path is needed for
1502+
// correct drop handling: without it, the coroutine drop handler may
1503+
// re-drop locals already dropped during unwinding. However, this is
1504+
// only relevant when the coroutine body contains yield points, because
1505+
// locals can only span a yield if one exists. For coroutines without
1506+
// yields (e.g. an async fn whose body has no `.await`), we skip
1507+
// `StorageDead` on unwind, avoiding O(n^2) MIR blowup. See #115327.
1508+
let storage_dead_on_unwind = self.coroutine_has_yields;
1509+
let invalidate_unwind = needs_drop || storage_dead_on_unwind;
1510+
let invalidate_dropline = needs_drop || self.coroutine.is_some();
14781511
for scope in self.scopes.scopes.iter_mut().rev() {
1479-
if invalidate_caches {
1480-
scope.invalidate_cache();
1512+
if invalidate_unwind {
1513+
scope.cached_unwind_block = None;
1514+
}
1515+
if invalidate_dropline {
1516+
scope.cached_coroutine_drop_block = None;
14811517
}
14821518

14831519
if scope.region_scope == region_scope {
@@ -1489,6 +1525,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
14891525
source_info: SourceInfo { span: scope_end, scope: scope.source_scope },
14901526
local,
14911527
kind: drop_kind,
1528+
storage_dead_on_unwind,
14921529
});
14931530

14941531
return;
@@ -1520,6 +1557,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
15201557
source_info: SourceInfo { span: scope_end, scope: scope.source_scope },
15211558
local,
15221559
kind: DropKind::ForLint,
1560+
storage_dead_on_unwind: false,
15231561
});
15241562

15251563
return;
@@ -1622,10 +1660,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
16221660
return cached_drop;
16231661
}
16241662

1625-
let is_coroutine = self.coroutine.is_some();
16261663
for scope in &mut self.scopes.scopes[uncached_scope..=target] {
16271664
for drop in &scope.drops {
1628-
if is_coroutine || drop.kind == DropKind::Value {
1665+
if drop.kind == DropKind::Value || drop.storage_dead_on_unwind {
16291666
cached_drop = self.scopes.unwind_drops.add_drop(*drop, cached_drop);
16301667
}
16311668
}
@@ -1811,7 +1848,6 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
18111848
/// instructions on unwinding)
18121849
/// * `dropline_to`, describes the drops that would occur at this point in the code if a
18131850
/// coroutine drop occurred.
1814-
/// * `storage_dead_on_unwind`, if true, then we should emit `StorageDead` even when unwinding
18151851
/// * `arg_count`, number of MIR local variables corresponding to fn arguments (used to assert that we don't drop those)
18161852
fn build_scope_drops<'tcx, F>(
18171853
cfg: &mut CFG<'tcx>,
@@ -1821,7 +1857,6 @@ fn build_scope_drops<'tcx, F>(
18211857
block: BasicBlock,
18221858
unwind_to: DropIdx,
18231859
dropline_to: Option<DropIdx>,
1824-
storage_dead_on_unwind: bool,
18251860
arg_count: usize,
18261861
is_async_drop: F,
18271862
) -> BlockAnd<()>
@@ -1844,10 +1879,10 @@ where
18441879
// drops panic (panicking while unwinding will abort, so there's no need for
18451880
// another set of arrows).
18461881
//
1847-
// For coroutines, we unwind from a drop on a local to its StorageDead
1848-
// statement. For other functions we don't worry about StorageDead. The
1849-
// drops for the unwind path should have already been generated by
1850-
// `diverge_cleanup_gen`.
1882+
// For drops in coroutines that were scheduled after a yield point, we
1883+
// unwind from a drop on a local to its StorageDead statement. For other
1884+
// drops we don't worry about StorageDead. The drops for the unwind path
1885+
// should have already been generated by `diverge_cleanup_gen`.
18511886

18521887
// `unwind_to` indicates what needs to be dropped should unwinding occur.
18531888
// This is a subset of what needs to be dropped when exiting the scope.
@@ -1922,7 +1957,7 @@ where
19221957
// so we can just leave `unwind_to` unmodified, but in some
19231958
// cases we emit things ALSO on the unwind path, so we need to adjust
19241959
// `unwind_to` in that case.
1925-
if storage_dead_on_unwind {
1960+
if drop_data.storage_dead_on_unwind {
19261961
debug_assert_eq!(
19271962
unwind_drops.drop_nodes[unwind_to].data.local,
19281963
drop_data.local
@@ -1953,10 +1988,11 @@ where
19531988
DropKind::Storage => {
19541989
// Ordinarily, storage-dead nodes are not emitted on unwind, so we don't
19551990
// need to adjust `unwind_to` on this path. However, in some specific cases
1956-
// we *do* emit storage-dead nodes on the unwind path, and in that case now that
1957-
// the storage-dead has completed, we need to adjust the `unwind_to` pointer
1958-
// so that any future drops we emit will not register storage-dead.
1959-
if storage_dead_on_unwind {
1991+
// (drops scheduled after a yield in a coroutine) we *do* emit storage-dead
1992+
// nodes on the unwind path, and in that case now that the storage-dead has
1993+
// completed, we need to adjust the `unwind_to` pointer so that any future
1994+
// drops we emit will not register storage-dead.
1995+
if drop_data.storage_dead_on_unwind {
19601996
debug_assert_eq!(
19611997
unwind_drops.drop_nodes[unwind_to].data.local,
19621998
drop_data.local

src/tools/compiletest/src/directives.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ mod directives {
260260
pub const ADD_MINICORE: &'static str = "add-minicore";
261261
pub const MINICORE_COMPILE_FLAGS: &'static str = "minicore-compile-flags";
262262
pub const DISABLE_GDB_PRETTY_PRINTERS: &'static str = "disable-gdb-pretty-printers";
263+
pub const TIMEOUT: &'static str = "timeout";
263264
pub const COMPARE_OUTPUT_BY_LINES: &'static str = "compare-output-by-lines";
264265
}
265266

@@ -957,6 +958,7 @@ pub(crate) fn make_test_description(
957958
let mut ignore = false;
958959
let mut ignore_message = None;
959960
let mut should_fail = false;
961+
let mut timeout_seconds = None;
960962

961963
// Scan through the test file to handle `ignore-*`, `only-*`, and `needs-*` directives.
962964
iter_directives(config, file_directives, &mut |ln @ &DirectiveLine { line_number, .. }| {
@@ -1004,6 +1006,13 @@ pub(crate) fn make_test_description(
10041006
}
10051007

10061008
should_fail |= config.parse_name_directive(ln, "should-fail");
1009+
1010+
if let Some(seconds) = config
1011+
.parse_name_value_directive(ln, directives::TIMEOUT)
1012+
.and_then(|value| value.trim().parse::<u64>().ok())
1013+
{
1014+
timeout_seconds = Some(seconds);
1015+
}
10071016
});
10081017

10091018
// The `should-fail` annotation doesn't apply to pretty tests,
@@ -1021,6 +1030,7 @@ pub(crate) fn make_test_description(
10211030
ignore,
10221031
ignore_message,
10231032
should_fail,
1033+
timeout_seconds,
10241034
}
10251035
}
10261036

src/tools/compiletest/src/directives/directive_names.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ pub(crate) const KNOWN_DIRECTIVE_NAMES: &[&str] = &[
289289
"should-ice",
290290
"stderr-per-bitwidth",
291291
"test-mir-pass",
292+
"timeout",
292293
"unique-doc-out-dir",
293294
"unset-exec-env",
294295
"unset-rustc-env",

src/tools/compiletest/src/executor.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use crate::panic_hook;
2121
mod deadline;
2222
mod json;
2323

24+
const DEFAULT_TEST_WARN_TIMEOUT_S: u64 = 60;
25+
2426
pub(crate) fn run_tests(config: &Config, tests: Vec<CollectedTest>) -> bool {
2527
let tests_len = tests.len();
2628
let filtered = filter_tests(config, tests);
@@ -52,7 +54,8 @@ pub(crate) fn run_tests(config: &Config, tests: Vec<CollectedTest>) -> bool {
5254
&& let Some((id, test)) = fresh_tests.next()
5355
{
5456
listener.test_started(test);
55-
deadline_queue.push(id, test);
57+
let timeout = test.desc.timeout_seconds.unwrap_or(DEFAULT_TEST_WARN_TIMEOUT_S);
58+
deadline_queue.push(id, test, timeout);
5659
let join_handle = spawn_test_thread(id, test, completion_tx.clone());
5760
running_tests.insert(id, RunningTest { test, join_handle });
5861
}
@@ -339,6 +342,7 @@ pub(crate) struct CollectedTestDesc {
339342
pub(crate) ignore: bool,
340343
pub(crate) ignore_message: Option<Cow<'static, str>>,
341344
pub(crate) should_fail: ShouldFail,
345+
pub(crate) timeout_seconds: Option<u64>,
342346
}
343347

344348
/// Tests with `//@ should-fail` are tests of compiletest itself, and should

src/tools/compiletest/src/executor/deadline.rs

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ use std::time::{Duration, Instant};
44

55
use crate::executor::{CollectedTest, TestId};
66

7-
const TEST_WARN_TIMEOUT_S: u64 = 60;
8-
97
struct DeadlineEntry<'a> {
108
id: TestId,
119
test: &'a CollectedTest,
@@ -27,11 +25,8 @@ impl<'a> DeadlineQueue<'a> {
2725
Instant::now()
2826
}
2927

30-
pub(crate) fn push(&mut self, id: TestId, test: &'a CollectedTest) {
31-
let deadline = self.now() + Duration::from_secs(TEST_WARN_TIMEOUT_S);
32-
if let Some(back) = self.queue.back() {
33-
assert!(back.deadline <= deadline);
34-
}
28+
pub(crate) fn push(&mut self, id: TestId, test: &'a CollectedTest, timeout_seconds: u64) {
29+
let deadline = self.now() + Duration::from_secs(timeout_seconds);
3530
self.queue.push_back(DeadlineEntry { id, test, deadline });
3631
}
3732

@@ -67,7 +62,7 @@ impl<'a> DeadlineQueue<'a> {
6762
}
6863

6964
fn next_deadline(&self) -> Option<Instant> {
70-
Some(self.queue.front()?.deadline)
65+
self.queue.iter().map(|entry| entry.deadline).min()
7166
}
7267

7368
fn for_each_entry_past_deadline(
@@ -77,26 +72,16 @@ impl<'a> DeadlineQueue<'a> {
7772
) {
7873
let now = self.now();
7974

80-
// Clear out entries that are past their deadline, but only invoke the
81-
// callback for tests that are still considered running.
82-
while let Some(entry) = pop_front_if(&mut self.queue, |entry| entry.deadline <= now) {
83-
if is_running(entry.id) {
75+
// Invoke callbacks for entries past their deadline that are still running.
76+
for entry in self.queue.iter() {
77+
if entry.deadline <= now && is_running(entry.id) {
8478
on_deadline_passed(entry.id, entry.test);
8579
}
8680
}
8781

88-
// Also clear out any leading entries that are no longer running, even
89-
// if their deadline hasn't been reached.
90-
while let Some(_) = pop_front_if(&mut self.queue, |entry| !is_running(entry.id)) {}
82+
// Remove entries that are past their deadline or no longer running.
83+
self.queue.retain(|entry| entry.deadline > now && is_running(entry.id));
9184

92-
if let Some(front) = self.queue.front() {
93-
assert!(now < front.deadline);
94-
}
85+
debug_assert!(self.queue.iter().all(|entry| now < entry.deadline));
9586
}
9687
}
97-
98-
/// FIXME(vec_deque_pop_if): Use `VecDeque::pop_front_if` when it is stable in bootstrap.
99-
fn pop_front_if<T>(queue: &mut VecDeque<T>, predicate: impl FnOnce(&T) -> bool) -> Option<T> {
100-
let first = queue.front()?;
101-
if predicate(first) { queue.pop_front() } else { None }
102-
}

tests/mir-opt/async_drop_live_dead.a-{closure#0}.coroutine_drop_async.0.panic-unwind.mir

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ fn a::{closure#0}(_1: Pin<&mut {async fn body of a<T>()}>, _2: &mut Context<'_>)
4444
}
4545

4646
bb3 (cleanup): {
47-
nop;
4847
nop;
4948
goto -> bb5;
5049
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// A regression test to ensure that an async function returning a large
2+
// `Vec<String>` compiles within a reasonable time. The quadratic MIR
3+
// blowup (#115327) manifseting itself earlier was caused by `StorageDead`
4+
// on the unwind path preventing tail-sharing of drop chains in coroutines.
5+
//
6+
// The test generates a source file containing `vec!["string".to_string(), ...]`
7+
// with 10,000 elements inside an async fn, then checks that it compiles.
8+
// The timeout is set such that the test will warn about it running too long
9+
// with the quadratic MIR growth. In the future time-outs could be converted
10+
// into hard failures in the compiletest machinery.
11+
12+
//@ timeout: 10
13+
14+
use std::fs;
15+
16+
use run_make_support::rustc;
17+
18+
const ELEMENT_COUNT: usize = 10_000;
19+
20+
fn main() {
21+
let vec_elements = r#""string".to_string(),"#.repeat(ELEMENT_COUNT);
22+
23+
let source = format!(
24+
r#"
25+
fn main() {{ produce_vector(); }}
26+
async fn produce_vector() -> Vec<String> {{
27+
vec![{vec_elements}]
28+
}}
29+
"#
30+
);
31+
32+
fs::write("generated.rs", &source).unwrap();
33+
rustc().edition("2021").input("generated.rs").run();
34+
}

0 commit comments

Comments
 (0)