Skip to content

Commit 34b17dd

Browse files
authored
[Distributed] Distributed remote ref should be as small as possible (swiftlang#87449)
Because we guaranteed that one cannot access a distribted "remote" instance's state via type system, we can allocate the instance much smaller because we never access those user initialized fields. This way a remote distributed actor reference is always 128 bytes, this is because actors have special storage 12*8 bytes of PrivateData in addition to the 16 bytes object header. With this optimization, regardless how many fields an actor has, the remote ref always is the same size. resolves rdar://81825648 Used claude to generate a bunch of tests to check the sizes of actors etc, I think this is correct (and getting it wrong totally just crashes immediately anyway). Could use a review though, thank you! cc @mikeash @xedin
1 parent fbad8aa commit 34b17dd

File tree

4 files changed

+56
-13
lines changed

4 files changed

+56
-13
lines changed

include/swift/Runtime/Metadata.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ bool swift_compareTypeContextDescriptors(const TypeContextDescriptor *lhs,
217217
const TypeContextDescriptor *rhs);
218218

219219
/// Compute the bounds of class metadata with a resilient superclass.
220+
SWIFT_EXPORT_FROM_ATTRIBUTE(swiftCore) // Cannot use SWIFT_RUNTIME_EXPORT because it is not compatible with C linkage
220221
ClassMetadataBounds getResilientMetadataBounds(
221222
const ClassDescriptor *descriptor);
222223
int32_t getResilientImmediateMembersOffset(const ClassDescriptor *descriptor);

stdlib/public/Concurrency/Actor.cpp

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,12 +1962,18 @@ void DefaultActorImpl::deallocate() {
19621962
#endif
19631963
}
19641964

1965+
static size_t
1966+
getDistributedRemoteActorAllocSize(const ClassMetadata *metadata);
1967+
19651968
void DefaultActorImpl::deallocateUnconditional() {
19661969
concurrency::trace::actor_deallocate(this);
19671970

19681971
#if !SWIFT_CONCURRENCY_EMBEDDED
19691972
auto metadata = cast<ClassMetadata>(this->metadata);
1970-
swift_deallocClassInstance(this, metadata->getInstanceSize(),
1973+
size_t deallocSize = isDistributedRemote() ?
1974+
getDistributedRemoteActorAllocSize(metadata)
1975+
: metadata->getInstanceSize();
1976+
swift_deallocClassInstance(this, deallocSize,
19711977
metadata->getInstanceAlignMask());
19721978
#else
19731979
// Embedded Swift's runtime doesn't actually use the size/mask values.
@@ -2299,12 +2305,17 @@ static bool isDefaultActorClass(const ClassMetadata *metadata) {
22992305
void swift::swift_defaultActor_deallocateResilient(HeapObject *actor) {
23002306
#if !SWIFT_CONCURRENCY_EMBEDDED
23012307
auto metadata = cast<ClassMetadata>(actor->metadata);
2308+
23022309
if (isDefaultActorClass(metadata))
23032310
return swift_defaultActor_deallocate(static_cast<DefaultActor*>(actor));
23042311

2305-
swift_deallocObject(actor, metadata->getInstanceSize(),
2312+
size_t deallocSize = swift_distributed_actor_is_remote(actor) ?
2313+
getDistributedRemoteActorAllocSize(metadata)
2314+
: metadata->getInstanceSize();
2315+
swift_deallocObject(actor, deallocSize,
23062316
metadata->getInstanceAlignMask());
23072317
#else
2318+
// TODO(distributed): Embedded should be able to handle remote actor references here
23082319
return swift_defaultActor_deallocate(static_cast<DefaultActor*>(actor));
23092320
#endif
23102321
}
@@ -2774,25 +2785,50 @@ void swift::swift_nonDefaultDistributedActor_initialize(NonDefaultDistributedAct
27742785
asImpl(_actor)->initialize();
27752786
}
27762787

2788+
/// Compute the minimal allocation size for a 'remote' distributed actor reference.
2789+
///
2790+
/// Remote distributed actors are proxy objects that only need storage for the
2791+
/// `id`, `actorSystem`, and `unownedExecutor` fields. Any user-defined stored
2792+
/// properties beyond those three are never initialized or accessed on a remote
2793+
/// instance, so we can trim the allocation at the offset where the first
2794+
/// user-defined field would begin.
2795+
static size_t
2796+
getDistributedRemoteActorAllocSize(const ClassMetadata *metadata) {
2797+
auto description = metadata->getDescription();
2798+
uint32_t numFields = description->NumFields;
2799+
2800+
assert(numFields >= 3 &&
2801+
"distributed actor must have at least id, actorSystem, and "
2802+
"unownedExecutor fields");
2803+
2804+
if (numFields >= 4) {
2805+
// The 4th field is the first user-defined stored property.
2806+
// Its offset marks the end of the synthesized fields,
2807+
// so it is exactly how much storage we need.
2808+
const auto *fieldOffsets = metadata->getFieldOffsets();
2809+
return fieldOffsets[3];
2810+
}
2811+
2812+
// Only the three required fields exist, remote-ref and local instances have the same size.
2813+
return metadata->getInstanceSize();
2814+
}
2815+
27772816
OpaqueValue*
27782817
swift::swift_distributedActor_remote_initialize(const Metadata *actorType) {
27792818
const ClassMetadata *metadata = actorType->getClassObject();
27802819

2781-
// TODO(distributed): make this allocation smaller
2782-
// ==== Allocate the memory for the remote instance
2820+
// Use a smaller allocation that only covers the id and actorSystem fields,
2821+
// since remote proxies never access user-defined stored properties.
2822+
size_t allocSize = getDistributedRemoteActorAllocSize(metadata);
2823+
assert(allocSize <= metadata->getInstanceSize() && "Remote reference size was larger than a local instance size!?");
27832824
HeapObject *alloc = swift_allocObject(metadata,
2784-
metadata->getInstanceSize(),
2825+
allocSize,
27852826
metadata->getInstanceAlignMask());
27862827

2787-
// TODO: remove this memset eventually, today we only do this to not have
2788-
// to modify the destructor logic, as releasing zeroes is no-op
2789-
memset((void *)(alloc + 1), 0,
2790-
metadata->getInstanceSize() - sizeof(HeapObject));
2828+
// Zero the body so that the destructor can safely release fields without
2829+
// encountering uninitialized memory.
2830+
memset((void *)(alloc + 1), 0, allocSize - sizeof(HeapObject));
27912831

2792-
// TODO(distributed): a remote one does not have to have the "real"
2793-
// default actor body, e.g. we don't need an executor at all; so
2794-
// we can allocate more efficiently and only share the flags/status field
2795-
// between the both memory representations
27962832
// If it is a default actor, we reuse the same layout as DefaultActorImpl,
27972833
// and store flags in the allocation directly as we initialize it.
27982834
if (isDefaultActorClass(metadata)) {

test/abi/macOS/arm64/stdlib.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,9 @@ Added: _$ss4SpanVsRi_zrlE22_makeBorrowingIteratorAByxGyF
12091209
Added: _$ss4SpanVyxGs18_BorrowingSequencesRi_zrlMc
12101210
Added: _$ss4SpanVyxGs26_BorrowingIteratorProtocolsRi_zrlMc
12111211

1212+
// Necessary for Distributed to compute distributed remote reference object size
1213+
Added: __ZN5swift26getResilientMetadataBoundsEPKNS_21TargetClassDescriptorINS_9InProcessEEE
1214+
12121215
// Borrow
12131216
Added: _$ss6BorrowVMa
12141217
Added: _$ss6BorrowVMn

test/abi/macOS/x86_64/stdlib.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,9 @@ Added: _$ss4SpanVsRi_zrlE22_makeBorrowingIteratorAByxGyF
12041204
Added: _$ss4SpanVyxGs18_BorrowingSequencesRi_zrlMc
12051205
Added: _$ss4SpanVyxGs26_BorrowingIteratorProtocolsRi_zrlMc
12061206

1207+
// Necessary for Distributed to compute distributed remote reference object size
1208+
Added: __ZN5swift26getResilientMetadataBoundsEPKNS_21TargetClassDescriptorINS_9InProcessEEE
1209+
12071210
// Borrow
12081211
Added: _$ss6BorrowVMa
12091212
Added: _$ss6BorrowVMn

0 commit comments

Comments
 (0)