Skip to content

Commit ae7bf2b

Browse files
committed
CoroFrame: Put escaped variables with multiple lifetimes on coroutine frame
The llvm.lifetime.start intrinsic guarantees that the address for a given alloca is always the same. So variables with escaped addresses reaching reaching a lifetime start/end block before and after a suspend must be placed onto the coroutine frame even if the variable itself is not alive across the suspend point. This computes a new `LoopKill` flag in the suspend crossing data flow anaysis to catch the case where a lifetime marker can reach itself via suspend-crossing path. This fixes https://llvm.org/PR52501 Differential Revision: https://reviews.llvm.org/D140231
1 parent 2f79f54 commit ae7bf2b

File tree

2 files changed

+127
-8
lines changed

2 files changed

+127
-8
lines changed

llvm/lib/Transforms/Coroutines/CoroFrame.cpp

+41-8
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,14 @@ class BlockToIndexMapping {
7777
//
7878
// For every basic block 'i' it maintains a BlockData that consists of:
7979
// Consumes: a bit vector which contains a set of indices of blocks that can
80-
// reach block 'i'
80+
// reach block 'i'. A block can trivially reach itself.
8181
// Kills: a bit vector which contains a set of indices of blocks that can
82-
// reach block 'i', but one of the path will cross a suspend point
82+
// reach block 'i' but there is a path crossing a suspend point
83+
// not repeating 'i' (path to 'i' without cycles containing 'i').
8384
// Suspend: a boolean indicating whether block 'i' contains a suspend point.
8485
// End: a boolean indicating whether block 'i' contains a coro.end intrinsic.
86+
// KillLoop: There is a path from 'i' to 'i' not otherwise repeating 'i' that
87+
// crosses a suspend point.
8588
//
8689
namespace {
8790
struct SuspendCrossingInfo {
@@ -92,6 +95,7 @@ struct SuspendCrossingInfo {
9295
BitVector Kills;
9396
bool Suspend = false;
9497
bool End = false;
98+
bool KillLoop = false;
9599
};
96100
SmallVector<BlockData, SmallVectorThreshold> Block;
97101

@@ -109,16 +113,31 @@ struct SuspendCrossingInfo {
109113

110114
SuspendCrossingInfo(Function &F, coro::Shape &Shape);
111115

112-
bool hasPathCrossingSuspendPoint(BasicBlock *DefBB, BasicBlock *UseBB) const {
113-
size_t const DefIndex = Mapping.blockToIndex(DefBB);
114-
size_t const UseIndex = Mapping.blockToIndex(UseBB);
115-
116-
bool const Result = Block[UseIndex].Kills[DefIndex];
117-
LLVM_DEBUG(dbgs() << UseBB->getName() << " => " << DefBB->getName()
116+
/// Returns true if there is a path from \p From to \p To crossing a suspend
117+
/// point without crossing \p From a 2nd time.
118+
bool hasPathCrossingSuspendPoint(BasicBlock *From, BasicBlock *To) const {
119+
size_t const FromIndex = Mapping.blockToIndex(From);
120+
size_t const ToIndex = Mapping.blockToIndex(To);
121+
bool const Result = Block[ToIndex].Kills[FromIndex];
122+
LLVM_DEBUG(dbgs() << From->getName() << " => " << To->getName()
118123
<< " answer is " << Result << "\n");
119124
return Result;
120125
}
121126

127+
/// Returns true if there is a path from \p From to \p To crossing a suspend
128+
/// point without crossing \p From a 2nd time. If \p From is the same as \p To
129+
/// this will also check if there is a looping path crossing a suspend point.
130+
bool hasPathOrLoopCrossingSuspendPoint(BasicBlock *From,
131+
BasicBlock *To) const {
132+
size_t const FromIndex = Mapping.blockToIndex(From);
133+
size_t const ToIndex = Mapping.blockToIndex(To);
134+
bool Result = Block[ToIndex].Kills[FromIndex] ||
135+
(From == To && Block[ToIndex].KillLoop);
136+
LLVM_DEBUG(dbgs() << From->getName() << " => " << To->getName()
137+
<< " answer is " << Result << " (path or loop)\n");
138+
return Result;
139+
}
140+
122141
bool isDefinitionAcrossSuspend(BasicBlock *DefBB, User *U) const {
123142
auto *I = cast<Instruction>(U);
124143

@@ -271,6 +290,7 @@ SuspendCrossingInfo::SuspendCrossingInfo(Function &F, coro::Shape &Shape)
271290
} else {
272291
// This is reached when S block it not Suspend nor coro.end and it
273292
// need to make sure that it is not in the kill set.
293+
S.KillLoop |= S.Kills[SuccNo];
274294
S.Kills.reset(SuccNo);
275295
}
276296

@@ -1440,6 +1460,19 @@ struct AllocaUseVisitor : PtrUseVisitor<AllocaUseVisitor> {
14401460
for (auto *S : LifetimeStarts)
14411461
if (Checker.isDefinitionAcrossSuspend(*S, I))
14421462
return true;
1463+
// Addresses are guaranteed to be identical after every lifetime.start so
1464+
// we cannot use the local stack if the address escaped and there is a
1465+
// suspend point between lifetime markers. This should also cover the
1466+
// case of a single lifetime.start intrinsic in a loop with suspend point.
1467+
if (PI.isEscaped()) {
1468+
for (auto *A : LifetimeStarts) {
1469+
for (auto *B : LifetimeStarts) {
1470+
if (Checker.hasPathOrLoopCrossingSuspendPoint(A->getParent(),
1471+
B->getParent()))
1472+
return true;
1473+
}
1474+
}
1475+
}
14431476
return false;
14441477
}
14451478
// FIXME: Ideally the isEscaped check should come at the beginning.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
2+
; RUN: opt < %s -passes='cgscc(coro-split),simplifycfg,early-cse' -S | FileCheck %s
3+
4+
@escape_hatch0 = external global i64
5+
@escape_hatch1 = external global i64
6+
7+
define void @foo() presplitcoroutine {
8+
; CHECK-LABEL: @foo(
9+
; CHECK-NEXT: entry:
10+
; CHECK-NEXT: [[STACKVAR0:%.*]] = alloca i64, align 8
11+
; CHECK-NEXT: [[ID:%.*]] = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr @foo.resumers)
12+
; CHECK-NEXT: [[ALLOC:%.*]] = call ptr @malloc(i64 40)
13+
; CHECK-NEXT: [[VFRAME:%.*]] = call noalias nonnull ptr @llvm.coro.begin(token [[ID]], ptr [[ALLOC]])
14+
; CHECK-NEXT: store ptr @foo.resume, ptr [[VFRAME]], align 8
15+
; CHECK-NEXT: [[DESTROY_ADDR:%.*]] = getelementptr inbounds [[FOO_FRAME:%.*]], ptr [[VFRAME]], i32 0, i32 1
16+
; CHECK-NEXT: store ptr @foo.destroy, ptr [[DESTROY_ADDR]], align 8
17+
; CHECK-NEXT: [[STACKVAR0_RELOAD_ADDR:%.*]] = getelementptr inbounds [[FOO_FRAME]], ptr [[VFRAME]], i32 0, i32 2
18+
; CHECK-NEXT: [[STACKVAR1_RELOAD_ADDR:%.*]] = getelementptr inbounds [[FOO_FRAME]], ptr [[VFRAME]], i32 0, i32 3
19+
; CHECK-NEXT: [[STACKVAR0_INT:%.*]] = ptrtoint ptr [[STACKVAR0_RELOAD_ADDR]] to i64
20+
; CHECK-NEXT: store i64 [[STACKVAR0_INT]], ptr @escape_hatch0, align 4
21+
; CHECK-NEXT: [[STACKVAR1_INT:%.*]] = ptrtoint ptr [[STACKVAR1_RELOAD_ADDR]] to i64
22+
; CHECK-NEXT: store i64 [[STACKVAR1_INT]], ptr @escape_hatch1, align 4
23+
; CHECK-NEXT: br label [[LOOP:%.*]]
24+
; CHECK: loop:
25+
; CHECK-NEXT: store i64 1234, ptr [[STACKVAR0_RELOAD_ADDR]], align 4
26+
; CHECK-NEXT: call void @bar()
27+
; CHECK-NEXT: [[INDEX_ADDR1:%.*]] = getelementptr inbounds [[FOO_FRAME]], ptr [[VFRAME]], i32 0, i32 4
28+
; CHECK-NEXT: store i1 false, ptr [[INDEX_ADDR1]], align 1
29+
; CHECK-NEXT: br i1 false, label [[LOOP]], label [[AFTERCOROEND:%.*]]
30+
; CHECK: AfterCoroEnd:
31+
; CHECK-NEXT: ret void
32+
;
33+
entry:
34+
%stackvar0 = alloca i64
35+
%stackvar1 = alloca i64
36+
37+
; address of %stackvar escapes and may be relied upon even after
38+
; suspending/resuming the coroutine regardless of the lifetime markers.
39+
%id = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)
40+
%size = call i64 @llvm.coro.size.i64()
41+
%alloc = call ptr @malloc(i64 %size)
42+
%vFrame = call noalias nonnull ptr @llvm.coro.begin(token %id, ptr %alloc)
43+
44+
; %stackvar0 must be rewritten to reference the coroutine Frame!
45+
%stackvar0_int = ptrtoint ptr %stackvar0 to i64
46+
store i64 %stackvar0_int, ptr @escape_hatch0
47+
; %stackvar1 must be rewritten to reference the coroutine Frame!
48+
%stackvar1_int = ptrtoint ptr %stackvar1 to i64
49+
store i64 %stackvar1_int, ptr @escape_hatch1
50+
51+
br label %loop
52+
53+
loop:
54+
call void @llvm.lifetime.start(i64 8, ptr %stackvar0)
55+
56+
store i64 1234, ptr %stackvar0
57+
58+
; Call could potentially change value in memory referenced by %stackvar0 /
59+
; %stackvar1 and rely on it staying the same across suspension.
60+
call void @bar()
61+
62+
call void @llvm.lifetime.end(i64 8, ptr %stackvar0)
63+
64+
%save = call token @llvm.coro.save(ptr null)
65+
%suspend = call i8 @llvm.coro.suspend(token %save, i1 false)
66+
switch i8 %suspend, label %exit [
67+
i8 0, label %loop
68+
i8 1, label %exit
69+
]
70+
71+
exit:
72+
call i1 @llvm.coro.end(ptr null, i1 false)
73+
ret void
74+
}
75+
76+
declare void @bar()
77+
declare ptr @malloc(i64)
78+
79+
declare token @llvm.coro.id(i32, ptr readnone, ptr nocapture readonly, ptr)
80+
declare i64 @llvm.coro.size.i64()
81+
declare ptr @llvm.coro.begin(token, ptr writeonly)
82+
declare token @llvm.coro.save(ptr)
83+
declare i8 @llvm.coro.suspend(token, i1)
84+
declare i1 @llvm.coro.end(ptr, i1)
85+
declare void @llvm.lifetime.start(i64, ptr nocapture)
86+
declare void @llvm.lifetime.end(i64, ptr nocapture)

0 commit comments

Comments
 (0)