Skip to content

Commit 0242d75

Browse files
committed
Delay deallocation of default actors when they're currently running.
For ordinary memory-management reasons, this should only ever happen when there will be no more uses of the actor outside of the actor runtime. The actor runtime, meanwhile, doesn't care about anything except the default-actor control state of the actor. So we can just allow the rest of the actor to be destructed when it isn't needed anymore, then destroy the actor state and deallocate the object when we get around to switching off the executor. This does assume that the task doesn't do anything which semantically detects the executor it's on before switching off it, since doing so might read a bogus executor. However, we should only get an executor in a zombie state like this when a hop has been removed or reordered, and detection events should count as inhibiting that and forcing the true executor to be switched to (and thus detected). (But maybe lifetime optimization can make this happen? Maybe we need semantic detection to filter out zombie executors.)
1 parent 95e90ea commit 0242d75

File tree

5 files changed

+207
-17
lines changed

5 files changed

+207
-17
lines changed

include/swift/Runtime/Concurrency.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,14 @@ void swift_defaultActor_initialize(DefaultActor *actor);
512512
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
513513
void swift_defaultActor_destroy(DefaultActor *actor);
514514

515+
/// Deallocate an instance of a default actor.
516+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
517+
void swift_defaultActor_deallocate(DefaultActor *actor);
518+
519+
/// Deallocate an instance of what might be a default actor.
520+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
521+
void swift_defaultActor_deallocateResilient(HeapObject *actor);
522+
515523
/// Enqueue a job on the default actor implementation.
516524
///
517525
/// The job must be ready to run. Notably, if it's a task, that

include/swift/Runtime/RuntimeFunctions.def

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1656,6 +1656,22 @@ FUNCTION(DefaultActorDestroy,
16561656
ARGS(RefCountedPtrTy),
16571657
ATTRS(NoUnwind))
16581658

1659+
// void swift_defaultActor_deallocate(DefaultActor *actor);
1660+
FUNCTION(DefaultActorDeallocate,
1661+
swift_defaultActor_deallocate, SwiftCC,
1662+
ConcurrencyAvailability,
1663+
RETURNS(VoidTy),
1664+
ARGS(RefCountedPtrTy),
1665+
ATTRS(NoUnwind))
1666+
1667+
// void swift_defaultActor_deallocateResilient(HeapObject *actor);
1668+
FUNCTION(DefaultActorDeallocateResilient,
1669+
swift_defaultActor_deallocateResilient, SwiftCC,
1670+
ConcurrencyAvailability,
1671+
RETURNS(VoidTy),
1672+
ARGS(RefCountedPtrTy),
1673+
ATTRS(NoUnwind))
1674+
16591675
//// void swift_taskGroup_initialize(AsyncTask *task, TaskGroup *group);
16601676
//FUNCTION(TaskGroupInitialize,
16611677
// swift_taskGroup_initialize, SwiftCC,

lib/IRGen/GenClass.cpp

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,15 +876,47 @@ static void getInstanceSizeAndAlignMask(IRGenFunction &IGF,
876876
= emitClassResilientInstanceSizeAndAlignMask(IGF, selfClass, metadata);
877877
}
878878

879+
static llvm::Value *emitCastToHeapObject(IRGenFunction &IGF,
880+
llvm::Value *value) {
881+
return IGF.Builder.CreateBitCast(value, IGF.IGM.RefCountedPtrTy);
882+
}
883+
879884
void irgen::emitClassDeallocation(IRGenFunction &IGF, SILType selfType,
880885
llvm::Value *selfValue) {
881886
auto *theClass = selfType.getClassOrBoundGenericClass();
882887

888+
// We want to deallocate default actors or potential default
889+
// actors differently. We assume that being a default actor
890+
// is purely a property of the root actor class, so just go to
891+
// that class.
892+
if (auto rootActorClass = theClass->getRootActorClass()) {
893+
// If it's a default actor, use swift_deallocDefaultActor.
894+
if (rootActorClass->isDefaultActor(IGF.IGM.getSwiftModule(),
895+
ResilienceExpansion::Maximal)) {
896+
selfValue = emitCastToHeapObject(IGF, selfValue);
897+
IGF.Builder.CreateCall(IGF.IGM.getDefaultActorDeallocateFn(),
898+
{selfValue});
899+
return;
900+
}
901+
902+
// If it's possibly a default actor, use a resilient pattern.
903+
if (!rootActorClass->isForeign() &&
904+
rootActorClass->isResilient(IGF.IGM.getSwiftModule(),
905+
ResilienceExpansion::Maximal)) {
906+
selfValue = emitCastToHeapObject(IGF, selfValue);
907+
IGF.Builder.CreateCall(IGF.IGM.getDefaultActorDeallocateResilientFn(),
908+
{selfValue});
909+
return;
910+
}
911+
912+
// Otherwise use the normal path.
913+
}
914+
883915
llvm::Value *size, *alignMask;
884916
getInstanceSizeAndAlignMask(IGF, selfType, theClass, selfValue,
885917
size, alignMask);
886918

887-
selfValue = IGF.Builder.CreateBitCast(selfValue, IGF.IGM.RefCountedPtrTy);
919+
selfValue = emitCastToHeapObject(IGF, selfValue);
888920
emitDeallocateClassInstance(IGF, selfValue, size, alignMask);
889921
}
890922

stdlib/public/Concurrency/Actor.cpp

Lines changed: 146 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -399,15 +399,28 @@ class DefaultActorImpl : public HeapObject {
399399

400400
/// There is currently a thread processing the actor at the stored
401401
/// max priority.
402-
Running
402+
Running,
403+
404+
/// The actor is a zombie that's been fully released but is still
405+
/// running. We delay deallocation until its running thread gives
406+
/// it up, which fortunately doesn't touch anything in the
407+
/// actor except for the DefaultActorImpl.
408+
///
409+
/// To coordinate between the releasing thread and the running
410+
/// thread, we have a two-stage "latch". This is because the
411+
/// releasing thread does not atomically decide to not deallocate.
412+
/// Fortunately almost all of the overhead here is only incurred
413+
/// when we actually do end up in the zombie state.
414+
Zombie_Latching,
415+
Zombie_ReadyForDeallocation
403416
};
404417

405418
struct Flags : public FlagSet<size_t> {
406419
enum : size_t {
407420
Status_offset = 0,
408-
Status_width = 2,
421+
Status_width = 3,
409422

410-
HasActiveInlineJob = 2,
423+
HasActiveInlineJob = 3,
411424

412425
MaxPriority = 8,
413426
MaxPriority_width = JobFlags::Priority_width,
@@ -420,6 +433,19 @@ class DefaultActorImpl : public HeapObject {
420433
FLAGSET_DEFINE_FIELD_ACCESSORS(Status_offset, Status_width, Status,
421434
getStatus, setStatus)
422435

436+
bool isAnyRunningStatus() const {
437+
auto status = getStatus();
438+
return status == Status::Running ||
439+
status == Status::Zombie_Latching ||
440+
status == Status::Zombie_ReadyForDeallocation;
441+
}
442+
443+
bool isAnyZombieStatus() const {
444+
auto status = getStatus();
445+
return status == Status::Zombie_Latching ||
446+
status == Status::Zombie_ReadyForDeallocation;
447+
}
448+
423449
/// Is there currently an active processing job allocated inline
424450
/// in the actor?
425451
FLAGSET_DEFINE_FLAG_ACCESSORS(HasActiveInlineJob,
@@ -459,10 +485,12 @@ class DefaultActorImpl : public HeapObject {
459485
}
460486

461487
/// Properly destruct an actor, except for the heap header.
462-
void destroy() {
463-
assert(CurrentState.load(std::memory_order_relaxed).Flags.getStatus()
464-
== Status::Idle && "actor not idle during destruction?");
465-
}
488+
void destroy();
489+
490+
/// Properly respond to the last release of a default actor. Note
491+
/// that the actor will have been completely torn down by the time
492+
/// we reach this point.
493+
void deallocate();
466494

467495
/// Add a job to this actor.
468496
void enqueue(Job *job);
@@ -477,6 +505,8 @@ class DefaultActorImpl : public HeapObject {
477505
Job *claimNextJobOrGiveUp(bool actorIsOwned, RunningJobInfo runner);
478506

479507
private:
508+
void deallocateUnconditional();
509+
480510
/// Schedule an inline processing job. This can generally only be
481511
/// done if we know nobody else is trying to do it at the same time,
482512
/// e.g. if this thread just sucessfully transitioned the actor from
@@ -531,6 +561,53 @@ static DefaultActor *asAbstract(DefaultActorImpl *actor) {
531561
/*********************** DEFAULT ACTOR IMPLEMENTATION ************************/
532562
/*****************************************************************************/
533563

564+
void DefaultActorImpl::destroy() {
565+
auto oldState = CurrentState.load(std::memory_order_relaxed);
566+
while (true) {
567+
assert(!oldState.FirstJob && "actor has queued jobs at destruction");
568+
if (oldState.Flags.getStatus() == Status::Idle) return;
569+
570+
assert(oldState.Flags.getStatus() == Status::Running &&
571+
"actor scheduled but not running at destruction");
572+
573+
// If the actor is currently running, set it to zombie status
574+
// so that we know to deallocate it when it's given up.
575+
auto newState = oldState;
576+
newState.Flags.setStatus(Status::Zombie_Latching);
577+
if (CurrentState.compare_exchange_weak(oldState, newState,
578+
std::memory_order_relaxed,
579+
std::memory_order_relaxed))
580+
return;
581+
}
582+
}
583+
584+
void DefaultActorImpl::deallocate() {
585+
// If we're in a zombie state waiting to latch, put the actor in the
586+
// ready-for-deallocation state, but don't actually deallocate yet.
587+
// Note that we should never see the actor already in the
588+
// ready-for-deallocation state; giving up the actor while in the
589+
// latching state will always put it in Idle state.
590+
auto oldState = CurrentState.load(std::memory_order_relaxed);
591+
while (oldState.Flags.getStatus() == Status::Zombie_Latching) {
592+
auto newState = oldState;
593+
newState.Flags.setStatus(Status::Zombie_ReadyForDeallocation);
594+
if (CurrentState.compare_exchange_weak(oldState, newState,
595+
std::memory_order_relaxed,
596+
std::memory_order_relaxed))
597+
return;
598+
}
599+
600+
assert(oldState.Flags.getStatus() == Status::Idle);
601+
602+
deallocateUnconditional();
603+
}
604+
605+
void DefaultActorImpl::deallocateUnconditional() {
606+
auto metadata = cast<ClassMetadata>(this->metadata);
607+
swift_deallocObject(this, metadata->getInstanceSize(),
608+
metadata->getInstanceAlignMask());
609+
}
610+
534611
/// Given that a job is enqueued normally on a default actor, get/set
535612
/// the next job in the actor's queue.
536613
///
@@ -888,17 +965,30 @@ void DefaultActorImpl::giveUpThread(RunningJobInfo runner) {
888965
fprintf(stderr, "[%p] giving up thread for actor %p\n", pthread_self(), this);
889966
#endif
890967
auto oldState = CurrentState.load(std::memory_order_acquire);
891-
assert(oldState.Flags.getStatus() == Status::Running);
968+
assert(oldState.Flags.isAnyRunningStatus());
892969

893970
ProcessOverrideJob *overridesToWake = nullptr;
894971
auto firstNewJob = preprocessQueue(oldState.FirstJob, JobRef(), nullptr,
895972
overridesToWake);
896973

897974
_swift_tsan_release(this);
898975
while (true) {
976+
// In Zombie_ReadyForDeallocation state, nothing else should
977+
// be touching the atomic, and there's no point updating it.
978+
if (oldState.Flags.getStatus() == Status::Zombie_ReadyForDeallocation) {
979+
wakeOverrides(overridesToWake, oldState.Flags.getMaxPriority());
980+
deallocateUnconditional();
981+
return;
982+
}
983+
984+
// In Zombie_Latching state, we should try to update to Idle;
985+
// if we beat the releasing thread, it'll deallocate.
986+
// In Running state, we may need to schedule a processing job.
987+
899988
State newState = oldState;
900989
newState.FirstJob = JobRef::getPreprocessed(firstNewJob);
901990
if (firstNewJob) {
991+
assert(oldState.Flags.getStatus() == Status::Running);
902992
newState.Flags.setStatus(Status::Scheduled);
903993
} else {
904994
newState.Flags.setStatus(Status::Idle);
@@ -946,7 +1036,7 @@ void DefaultActorImpl::giveUpThread(RunningJobInfo runner) {
9461036
// The priority of the remaining work.
9471037
auto newPriority = newState.Flags.getMaxPriority();
9481038

949-
// Wake any overrides.
1039+
// Process the override commands we found.
9501040
wakeOverrides(overridesToWake, newPriority);
9511041

9521042
// This is the actor's owning job; per the ownership rules (see
@@ -983,13 +1073,16 @@ Job *DefaultActorImpl::claimNextJobOrGiveUp(bool actorIsOwned,
9831073

9841074
// The status had better be Running unless we're trying to acquire
9851075
// our first job.
986-
assert(oldState.Flags.getStatus() == Status::Running ||
987-
!actorIsOwned);
1076+
assert(oldState.Flags.isAnyRunningStatus() || !actorIsOwned);
9881077

9891078
// If we don't yet own the actor, we need to try to claim the actor
9901079
// first; we cannot safely access the queue memory yet because other
9911080
// threads may concurrently be trying to do this.
9921081
if (!actorIsOwned) {
1082+
// We really shouldn't ever be in a state where we're trying to take
1083+
// over a non-running actor if the actor is in a zombie state.
1084+
assert(!oldState.Flags.isAnyZombieStatus());
1085+
9931086
while (true) {
9941087
// A helper function when the only change we need to try is to
9951088
// update for an inline runner.
@@ -1061,7 +1154,7 @@ Job *DefaultActorImpl::claimNextJobOrGiveUp(bool actorIsOwned,
10611154
}
10621155
}
10631156

1064-
assert(oldState.Flags.getStatus() == Status::Running);
1157+
assert(oldState.Flags.isAnyRunningStatus());
10651158

10661159
// We should have taken care of the inline-job bookkeeping now.
10671160
assert(!oldState.Flags.hasActiveInlineJob() || !runner.wasInlineJob());
@@ -1075,6 +1168,14 @@ Job *DefaultActorImpl::claimNextJobOrGiveUp(bool actorIsOwned,
10751168
Optional<JobPriority> remainingJobPriority;
10761169
_swift_tsan_release(this);
10771170
while (true) {
1171+
// In Zombie_ReadyForDeallocation state, nothing else should
1172+
// be touching the atomic, and there's no point updating it.
1173+
if (oldState.Flags.getStatus() == Status::Zombie_ReadyForDeallocation) {
1174+
wakeOverrides(overridesToWake, oldState.Flags.getMaxPriority());
1175+
deallocateUnconditional();
1176+
return nullptr;
1177+
}
1178+
10781179
State newState = oldState;
10791180

10801181
// If the priority we're currently running with is adqeuate for
@@ -1300,6 +1401,8 @@ void DefaultActorImpl::enqueue(Job *job) {
13001401
OverrideJobCache overrideJob;
13011402

13021403
while (true) {
1404+
assert(!oldState.Flags.isAnyZombieStatus() &&
1405+
"enqueuing work on a zombie actor");
13031406
auto newState = oldState;
13041407

13051408
// Put the job at the front of the job list (which will get
@@ -1387,6 +1490,9 @@ bool DefaultActorImpl::tryAssumeThread(RunningJobInfo runner) {
13871490
}
13881491
}
13891492

1493+
assert(!oldState.Flags.isAnyZombieStatus() &&
1494+
"trying to assume a zombie actor");
1495+
13901496
return false;
13911497
}
13921498

@@ -1402,6 +1508,34 @@ void swift::swift_defaultActor_enqueue(Job *job, DefaultActor *_actor) {
14021508
asImpl(_actor)->enqueue(job);
14031509
}
14041510

1511+
void swift::swift_defaultActor_deallocate(DefaultActor *_actor) {
1512+
asImpl(_actor)->deallocate();
1513+
}
1514+
1515+
static bool isDefaultActorClass(const ClassMetadata *metadata) {
1516+
assert(metadata->isTypeMetadata());
1517+
while (true) {
1518+
// Trust the class descriptor if it says it's a default actor.
1519+
if (metadata->getDescription()->isDefaultActor())
1520+
return true;
1521+
1522+
// Go to the superclass.
1523+
metadata = metadata->Superclass;
1524+
1525+
// If we run out of Swift classes, it's not a default actor.
1526+
if (!metadata || !metadata->isTypeMetadata()) return false;
1527+
}
1528+
}
1529+
1530+
void swift::swift_defaultActor_deallocateResilient(HeapObject *actor) {
1531+
auto metadata = cast<ClassMetadata>(actor->metadata);
1532+
if (isDefaultActorClass(metadata))
1533+
return swift_defaultActor_deallocate(static_cast<DefaultActor*>(actor));
1534+
1535+
swift_deallocObject(actor, metadata->getInstanceSize(),
1536+
metadata->getInstanceAlignMask());
1537+
}
1538+
14051539
/// FIXME: only exists for the quick-and-dirty MainActor implementation.
14061540
namespace swift {
14071541
Metadata* MainActorMetadata = nullptr;

test/IRGen/async/default_actor.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,24 @@ import resilient_actor
2323

2424
// CHECK-LABEL: define hidden swiftcc void @"$s13default_actor1ACfD"(%T13default_actor1AC* swiftself %0)
2525
// CHECK-NOT: ret void
26-
// CHECK: call swiftcc void @swift_deallocObject(
26+
// CHECK: call swiftcc void @swift_defaultActor_deallocate(
2727
// CHECK: ret void
2828
actor A {}
2929

3030
// CHECK-LABEL: define hidden swiftcc void @"$s13default_actor1BCfD"(%T13default_actor1BC* swiftself %0)
3131
// CHECK-NOT: ret void
32-
// CHECK: call swiftcc void @swift_deallocObject(
32+
// CHECK: call swiftcc void @swift_defaultActor_deallocateResilient(
3333
// CHECK: ret void
3434
actor B : ResilientBaseActor {}
3535

3636
// CHECK-LABEL: define hidden swiftcc void @"$s13default_actor1CCfD"(%T13default_actor1CC* swiftself %0)
3737
// CHECK-NOT: ret void
38-
// CHECK: call swiftcc void @swift_deallocObject(
38+
// CHECK: call swiftcc void @swift_defaultActor_deallocateResilient(
3939
// CHECK: ret void
4040
actor C : FixedSubclassOfResilientBaseActor {}
4141

4242
// CHECK-LABEL: define hidden swiftcc void @"$s13default_actor1DCfD"(%T13default_actor1DC* swiftself %0)
4343
// CHECK-NOT: ret void
44-
// CHECK: call swiftcc void @swift_deallocObject(
44+
// CHECK: call swiftcc void @swift_defaultActor_deallocate(
4545
// CHECK: ret void
4646
actor D : FixedBaseActor {}

0 commit comments

Comments
 (0)