Skip to content

Commit 7611ecc

Browse files
authored
Merge pull request swiftlang#36975 from eeckstein/optimize-async-let
concurrency: make the startAsyncLet closure no-escaping
2 parents 35707c3 + 93367ed commit 7611ecc

File tree

15 files changed

+186
-95
lines changed

15 files changed

+186
-95
lines changed

include/swift/ABI/Task.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -582,13 +582,13 @@ class FutureAsyncContext : public AsyncContext {
582582
/// This matches the ABI of a closure `() async throws -> ()`
583583
using AsyncVoidClosureEntryPoint =
584584
SWIFT_CC(swiftasync)
585-
void (SWIFT_ASYNC_CONTEXT AsyncContext *, SWIFT_CONTEXT HeapObject *);
585+
void (SWIFT_ASYNC_CONTEXT AsyncContext *, SWIFT_CONTEXT void *);
586586

587587
/// This matches the ABI of a closure `<T>() async throws -> T`
588588
using AsyncGenericClosureEntryPoint =
589589
SWIFT_CC(swiftasync)
590590
void(OpaqueValue *,
591-
SWIFT_ASYNC_CONTEXT AsyncContext *, SWIFT_CONTEXT HeapObject *);
591+
SWIFT_ASYNC_CONTEXT AsyncContext *, SWIFT_CONTEXT void *);
592592

593593
/// This matches the ABI of the resume function of a closure
594594
/// `() async throws -> ()`.
@@ -602,7 +602,7 @@ class AsyncContextPrefix {
602602
// passing the closure context instead of via the async context)
603603
AsyncVoidClosureEntryPoint *__ptrauth_swift_task_resume_function
604604
asyncEntryPoint;
605-
HeapObject *closureContext;
605+
void *closureContext;
606606
SwiftError *errorResult;
607607
};
608608

@@ -615,7 +615,7 @@ class FutureAsyncContextPrefix {
615615
// passing the closure context instead of via the async context)
616616
AsyncGenericClosureEntryPoint *__ptrauth_swift_task_resume_function
617617
asyncEntryPoint;
618-
HeapObject *closureContext;
618+
void *closureContext;
619619
SwiftError *errorResult;
620620
};
621621

include/swift/AST/Builtins.def

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,9 @@ BUILTIN_MISC_OPERATION(StartAsyncLet, "startAsyncLet", "", Special)
793793
/// asyncLetEnd(): (Builtin.RawPointer) -> Void
794794
///
795795
/// Ends and destroys an async-let.
796+
/// The ClosureLifetimeFixup pass adds a second operand to the builtin to
797+
/// ensure that optimizations keep the stack-allocated closure arguments alive
798+
/// until the endAsyncLet.
796799
BUILTIN_MISC_OPERATION_WITH_SILGEN(EndAsyncLet, "endAsyncLet", "", Special)
797800

798801
/// createAsyncTaskFuture(): (

include/swift/Runtime/Concurrency.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,15 +281,15 @@ bool swift_taskGroup_isEmpty(TaskGroup *group);
281281
/// \code
282282
/// func swift_asyncLet_start<T>(
283283
/// _ alet: Builtin.RawPointer,
284-
/// operation: __owned @Sendable @escaping () async throws -> T
284+
/// operation: __owned @Sendable () async throws -> T
285285
/// )
286286
/// \endcode
287287
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
288288
void swift_asyncLet_start(
289289
AsyncLet *alet,
290290
const Metadata *futureResultType,
291291
void *closureEntryPoint,
292-
HeapObject * /* +1 */ closureContext);
292+
void *closureContext);
293293

294294
/// This matches the ABI of a closure `<T>(Builtin.RawPointer) async -> T`
295295
using AsyncLetWaitSignature =

include/swift/Runtime/RuntimeFunctions.def

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,7 +1682,7 @@ FUNCTION(DefaultActorDeallocateResilient,
16821682
/// AsyncLet *alet,
16831683
/// const Metadata *futureResultType,
16841684
/// void *closureEntryPoint,
1685-
/// HeapObject *closureContext
1685+
/// OpaqueValue *closureContext
16861686
/// );
16871687
FUNCTION(AsyncLetStart,
16881688
swift_asyncLet_start, SwiftCC,
@@ -1691,7 +1691,7 @@ FUNCTION(AsyncLetStart,
16911691
ARGS(SwiftAsyncLetPtrTy, // AsyncLet*, alias for Int8PtrTy
16921692
TypeMetadataPtrTy, // futureResultType
16931693
Int8PtrTy, // closureEntry
1694-
RefCountedPtrTy, // closureContext
1694+
OpaquePtrTy, // closureContext
16951695
),
16961696
ATTRS(NoUnwind, ArgMemOnly))
16971697

lib/AST/Builtins.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1496,7 +1496,7 @@ static ValueDecl *getStartAsyncLet(ASTContext &ctx, Identifier id) {
14961496
builder.addParameter(makeConcrete(OptionalType::get(ctx.TheRawPointerType)));
14971497

14981498
// operation async function pointer: () async throws -> T
1499-
auto extInfo = ASTExtInfoBuilder().withAsync().withThrows().build();
1499+
auto extInfo = ASTExtInfoBuilder().withAsync().withThrows().withNoEscape().build();
15001500
builder.addParameter(
15011501
makeConcrete(FunctionType::get({ }, genericParam, extInfo)));
15021502

lib/IRGen/GenBuiltin.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,9 @@ void irgen::emitBuiltinCall(IRGenFunction &IGF, const BuiltinInfo &Builtin,
244244

245245
if (Builtin.ID == BuiltinValueKind::EndAsyncLet) {
246246
emitEndAsyncLet(IGF, args.claimNext());
247+
// Ignore a second operand which is inserted by ClosureLifetimeFixup and
248+
// only used for dependency tracking.
249+
(void)args.claimAll();
247250
return;
248251
}
249252

lib/SILOptimizer/Mandatory/ClosureLifetimeFixup.cpp

Lines changed: 113 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,61 @@ static SILValue insertMarkDependenceForCapturedArguments(PartialApplyInst *pai,
363363
return curr;
364364
}
365365

366+
/// Returns the (single) "endAsyncLet" builtin if \p startAsyncLet is a
367+
/// "startAsyncLet" builtin.
368+
static BuiltinInst *getEndAsyncLet(BuiltinInst *startAsyncLet) {
369+
if (startAsyncLet->getBuiltinKind() != BuiltinValueKind::StartAsyncLet)
370+
return nullptr;
371+
372+
BuiltinInst *endAsyncLet = nullptr;
373+
for (Operand *op : startAsyncLet->getUses()) {
374+
auto *endBI = dyn_cast<BuiltinInst>(op->getUser());
375+
if (endBI && endBI->getBuiltinKind() == BuiltinValueKind::EndAsyncLet) {
376+
// At this stage of the pipeline, it's always the case that a
377+
// startAsyncLet has an endAsyncLet: that's how SILGen generates it.
378+
// Just to be on the safe side, do this check.
379+
if (endAsyncLet)
380+
return nullptr;
381+
endAsyncLet = endBI;
382+
}
383+
}
384+
return endAsyncLet;
385+
}
386+
387+
/// Call the \p insertFn with a builder at all insertion points after
388+
/// a closure is used by \p closureUser.
389+
static void insertAfterClosureUser(SILInstruction *closureUser,
390+
function_ref<void(SILBuilder &)> insertFn) {
391+
// Don't insert any destroy or deallocation right before an unreachable.
392+
// It's not needed an will only add up to code size.
393+
auto insertAtNonUnreachable = [&](SILBuilder &builder) {
394+
if (isa<UnreachableInst>(builder.getInsertionPoint()))
395+
return;
396+
insertFn(builder);
397+
};
398+
399+
if (auto *startAsyncLet = dyn_cast<BuiltinInst>(closureUser)) {
400+
BuiltinInst *endAsyncLet = getEndAsyncLet(startAsyncLet);
401+
assert(endAsyncLet);
402+
SILBuilderWithScope builder(std::next(endAsyncLet->getIterator()));
403+
insertAtNonUnreachable(builder);
404+
return;
405+
}
406+
FullApplySite fas = FullApplySite::isa(closureUser);
407+
assert(fas);
408+
fas.insertAfterFullEvaluation(insertAtNonUnreachable);
409+
}
410+
411+
static SILValue skipConvert(SILValue v) {
412+
auto *cvt = dyn_cast<ConvertFunctionInst>(v);
413+
if (!cvt)
414+
return v;
415+
auto *pa = dyn_cast<PartialApplyInst>(cvt->getOperand());
416+
if (!pa || !pa->hasOneUse())
417+
return v;
418+
return pa;
419+
}
420+
366421
/// Rewrite a partial_apply convert_escape_to_noescape sequence with a single
367422
/// apply/try_apply user to a partial_apply [stack] terminated with a
368423
/// dealloc_stack placed after the apply.
@@ -386,12 +441,15 @@ static SILValue insertMarkDependenceForCapturedArguments(PartialApplyInst *pai,
386441
/// dealloc_stack still needs to be balanced with other dealloc_stacks i.e the
387442
/// caller needs to use the StackNesting utility to update the dealloc_stack
388443
/// nesting.
389-
static bool tryRewriteToPartialApplyStack(
390-
SILLocation &loc, PartialApplyInst *origPA,
391-
ConvertEscapeToNoEscapeInst *cvt, SILInstruction *singleApplyUser,
392-
SILBasicBlock::iterator &advanceIfDelete,
444+
static SILValue tryRewriteToPartialApplyStack(
445+
ConvertEscapeToNoEscapeInst *cvt,
446+
SILInstruction *closureUser, SILBasicBlock::iterator &advanceIfDelete,
393447
llvm::DenseMap<SILInstruction *, SILInstruction *> &memoized) {
394-
448+
449+
auto *origPA = dyn_cast<PartialApplyInst>(skipConvert(cvt->getOperand()));
450+
if (!origPA)
451+
return SILValue();
452+
395453
auto *convertOrPartialApply = cast<SingleValueInstruction>(origPA);
396454
if (cvt->getOperand() != origPA)
397455
convertOrPartialApply = cast<ConvertFunctionInst>(cvt->getOperand());
@@ -415,7 +473,7 @@ static bool tryRewriteToPartialApplyStack(
415473
continue;
416474
}
417475
if (singleNonDebugNonRefCountUser)
418-
return false;
476+
return SILValue();
419477
singleNonDebugNonRefCountUser = user;
420478
}
421479

@@ -480,39 +538,15 @@ static bool tryRewriteToPartialApplyStack(
480538
}
481539
}
482540

483-
// Insert destroys of arguments after the apply and the dealloc_stack.
484-
if (auto *apply = dyn_cast<ApplyInst>(singleApplyUser)) {
485-
auto insertPt = std::next(SILBasicBlock::iterator(apply));
486-
// Don't insert dealloc_stacks at unreachable.
487-
if (isa<UnreachableInst>(*insertPt))
488-
return true;
489-
SILBuilderWithScope b3(insertPt);
490-
b3.createDeallocStack(loc, newPA);
491-
insertDestroyOfCapturedArguments(newPA, b3);
541+
// Insert destroys of arguments after the closure user and the dealloc_stack.
542+
insertAfterClosureUser(closureUser, [newPA](SILBuilder &builder) {
543+
auto loc = RegularLocation(builder.getInsertionPointLoc());
544+
builder.createDeallocStack(loc, newPA);
545+
insertDestroyOfCapturedArguments(newPA, builder);
492546
// dealloc_stack of the in_guaranteed capture is inserted
493-
insertDeallocOfCapturedArguments(newPA, b3);
494-
} else if (auto *tai = dyn_cast<TryApplyInst>(singleApplyUser)) {
495-
for (auto *succBB : tai->getSuccessorBlocks()) {
496-
SILBuilderWithScope b3(succBB->begin());
497-
b3.createDeallocStack(loc, newPA);
498-
insertDestroyOfCapturedArguments(newPA, b3);
499-
// dealloc_stack of the in_guaranteed capture is inserted
500-
insertDeallocOfCapturedArguments(newPA, b3);
501-
}
502-
} else {
503-
llvm_unreachable("Unknown FullApplySite instruction kind");
504-
}
505-
return true;
506-
}
507-
508-
static SILValue skipConvert(SILValue v) {
509-
auto *cvt = dyn_cast<ConvertFunctionInst>(v);
510-
if (!cvt)
511-
return v;
512-
auto *pa = dyn_cast<PartialApplyInst>(cvt->getOperand());
513-
if (!pa || !pa->hasOneUse())
514-
return v;
515-
return pa;
547+
insertDeallocOfCapturedArguments(newPA, builder);
548+
});
549+
return closure;
516550
}
517551

518552
static bool tryExtendLifetimeToLastUse(
@@ -525,45 +559,52 @@ static bool tryExtendLifetimeToLastUse(
525559
if (!singleUser)
526560
return false;
527561

528-
// Handle an apply.
529-
if (auto singleApplyUser = FullApplySite::isa(singleUser)) {
530-
// FIXME: Don't know how-to handle begin_apply/end_apply yet.
531-
if (isa<BeginApplyInst>(singleApplyUser.getInstruction())) {
562+
// Handle apply instructions and startAsyncLet.
563+
BuiltinInst *endAsyncLet = nullptr;
564+
if (FullApplySite::isa(singleUser)) {
565+
// TODO: Enable begin_apply/end_apply. It should work, but is not tested yet.
566+
if (isa<BeginApplyInst>(singleUser))
532567
return false;
533-
}
534-
535-
auto loc = RegularLocation::getAutoGeneratedLocation();
536-
auto origPA = dyn_cast<PartialApplyInst>(skipConvert(cvt->getOperand()));
537-
if (origPA && tryRewriteToPartialApplyStack(
538-
loc, origPA, cvt, singleApplyUser.getInstruction(),
539-
advanceIfDelete, memoized))
540-
return true;
568+
} else if (auto *bi = dyn_cast<BuiltinInst>(singleUser)) {
569+
endAsyncLet = getEndAsyncLet(bi);
570+
if (!endAsyncLet)
571+
return false;
572+
} else {
573+
return false;
574+
}
541575

542-
// Insert a copy at the convert_escape_to_noescape [not_guaranteed] and
543-
// change the instruction to the guaranteed form.
544-
auto escapingClosure = cvt->getOperand();
545-
auto *closureCopy =
546-
SILBuilderWithScope(cvt).createCopyValue(loc, escapingClosure);
547-
cvt->setLifetimeGuaranteed();
548-
cvt->setOperand(closureCopy);
549-
550-
// Insert a destroy after the apply.
551-
if (auto *apply = dyn_cast<ApplyInst>(singleApplyUser.getInstruction())) {
552-
auto insertPt = std::next(SILBasicBlock::iterator(apply));
553-
SILBuilderWithScope(insertPt).createDestroyValue(loc, closureCopy);
554-
555-
} else if (auto *tai =
556-
dyn_cast<TryApplyInst>(singleApplyUser.getInstruction())) {
557-
for (auto *succBB : tai->getSuccessorBlocks()) {
558-
SILBuilderWithScope(succBB->begin())
559-
.createDestroyValue(loc, closureCopy);
560-
}
561-
} else {
562-
llvm_unreachable("Unknown FullApplySite instruction kind");
576+
if (SILValue closure = tryRewriteToPartialApplyStack(cvt, singleUser,
577+
advanceIfDelete, memoized)) {
578+
if (auto *cfi = dyn_cast<ConvertFunctionInst>(closure))
579+
closure = cfi->getOperand();
580+
if (endAsyncLet && isa<MarkDependenceInst>(closure)) {
581+
// Add the top-level mark_dependence (which keeps the closure arguments
582+
// alive) as a second operand to the endAsyncLet builtin.
583+
// This ensures that the closure arguments are kept alive until the
584+
// endAsyncLet builtin.
585+
assert(endAsyncLet->getNumOperands() == 1);
586+
SILBuilderWithScope builder(endAsyncLet);
587+
builder.createBuiltin(endAsyncLet->getLoc(), endAsyncLet->getName(),
588+
endAsyncLet->getType(), endAsyncLet->getSubstitutions(),
589+
{endAsyncLet->getOperand(0), closure});
590+
endAsyncLet->eraseFromParent();
563591
}
564592
return true;
565593
}
566-
return false;
594+
595+
// Insert a copy at the convert_escape_to_noescape [not_guaranteed] and
596+
// change the instruction to the guaranteed form.
597+
auto escapingClosure = cvt->getOperand();
598+
auto *closureCopy =
599+
SILBuilderWithScope(cvt).createCopyValue(cvt->getLoc(), escapingClosure);
600+
cvt->setLifetimeGuaranteed();
601+
cvt->setOperand(closureCopy);
602+
603+
insertAfterClosureUser(singleUser, [closureCopy](SILBuilder &builder) {
604+
auto loc = RegularLocation(builder.getInsertionPointLoc());
605+
builder.createDestroyValue(loc, closureCopy);
606+
});
607+
return true;
567608
}
568609

569610
/// Ensure the lifetime of the closure across a two step optional conversion

stdlib/public/CompatibilityOverride/CompatibilityOverrideConcurrency.def

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ OVERRIDE_TASK(task_create_group_future_common, AsyncTaskAndContext, , , ,
100100
(JobFlags flags, TaskGroup *group,
101101
const Metadata *futureResultType,
102102
FutureAsyncSignature::FunctionType *function,
103-
HeapObject */* +1 */ closureContext, size_t initialContextSize),
103+
void *closureContext, bool owningClosureContext,
104+
size_t initialContextSize),
104105
(flags, group, futureResultType, function, closureContext,
105-
initialContextSize))
106+
owningClosureContext, initialContextSize))
106107

107108
OVERRIDE_TASK(task_future_wait, void, SWIFT_EXPORT_FROM(swift_Concurrency),
108109
SWIFT_CC(swiftasync), swift::,
@@ -155,7 +156,7 @@ OVERRIDE_ASYNC_LET(asyncLet_start, void,
155156
(AsyncLet *alet,
156157
const Metadata *futureResultType,
157158
void *closureEntryPoint,
158-
HeapObject */* +1 */ closureContext
159+
void *closureContext
159160
),
160161
(alet, futureResultType,
161162
closureEntryPoint, closureContext))

stdlib/public/Concurrency/AsyncLet.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,15 @@ SWIFT_CC(swift)
9797
static void swift_asyncLet_startImpl(AsyncLet *alet,
9898
const Metadata *futureResultType,
9999
void *closureEntryPoint,
100-
HeapObject * /* +1 */ closureContext) {
100+
void *closureContext) {
101101
AsyncTask *parent = swift_task_getCurrent();
102102
assert(parent && "async-let cannot be created without parent task");
103103

104104
auto flags = JobFlags(JobKind::Task, parent->getPriority());
105105
flags.task_setIsFuture(true);
106106
flags.task_setIsChildTask(true);
107107

108-
auto childTaskAndContext = swift_task_create_future(
108+
auto childTaskAndContext = swift_task_create_future_no_escaping(
109109
flags,
110110
futureResultType,
111111
closureEntryPoint,

stdlib/public/Concurrency/AsyncLet.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Swift
2020
@_silgen_name("swift_asyncLet_start")
2121
public func _asyncLetStart<T>(
2222
asyncLet: Builtin.RawPointer,
23-
operation: __owned @Sendable @escaping () async throws -> T
23+
operation: @Sendable () async throws -> T
2424
)
2525

2626
/// Similar to _taskFutureGet but for AsyncLet

0 commit comments

Comments
 (0)