I’m aware that Swift’s existential any has performance overhead. From SE-335:
Existential types are also significantly more expensive than using concrete types. Because they can store any value whose type conforms to the protocol, and the type of value stored can change dynamically, existential types require dynamic memory unless the value is small enough to fit within an inline 3-word buffer. In addition to heap allocation and reference counting, code using existential types incurs pointer indirection and dynamic method dispatch that cannot be optimized away.
I’m trying to put that overhead into context by comparing it with alternatives. Suppose I’m building a data persistence framework and need model objects to have a consistent api. I can do that with a protocol:
protocol Persistable: AnyObject
{
var id: UUID { get set }
func save()
}
Or I can force model objects to inherit from a base class to ensure they have the consistent API I need:
open class ModelObject
{
final var id: UUID
open func save() { … }
}
SE-335 implies that the protocol approach is less performant, overall, than the subclassing approach. Is this the case?
in your example, you have constrained the existential to AnyObject which should enable it to achieve similar performance to the class inheritance-based approach.
But the class inheritance approach is still more performant?
I guess I just don’t have a good intuition for why casting an any SomeProtocol involves more overhead than dynamic dispatch.
For example, consider calling save() on an any Persistable vs a ModelObject subclass: in both cases we’re saying, “It’s a thing that implements save(), so go look up exactly what concrete type it is and then call that type’s implementation of save().”
Why does the protocol approach involve more than dynamic dispatch?
I’m not sure entirely, but either using existentials or relying on inheritance with a base class has no difference since both use dynamic dispatch to achieve the behavior.
The quoted excerpt is making a case on the static dispatch vs dynamic, with the latter getting into play with existentials. The visible performance benefit might occur with static dispatch, since in that case all types is resolved at compilation time, removing the need to use witness table (akin to vtable in C++) to resolve hierarchy of types in runtime.
In order to rely on static dispatch, you have to replace use of existentials with use of concrete (including generics) types. In practice that not an easy task, especially for such use cases as you present here with data persistence, where you are more likely won’t be able to avoid to resort to generics, for instance, to store a collection of objects.
You also can look at SwiftData (and Vapor’s ORM if I’m not mistaken as well) case which uses classes and inheritance existentials (corrected) for the exact same use case, which for me signals about at least two things:
There is no significant performance gain for the use case to give yourself (and end users) trouble of requiring a lot of generic code.
Complexity (I’d probably even say lack of possibility) of implementation is insane for the given task.
So you can try benefit from static dispatch by avoiding existentials where this brings little to no trouble, but that’s probably not the case. Also, neither option is inherently bad, both solutions present tradeoffs to consider in each case. The case with existentials in Swift is more about them being implicit and overused in many places where they can be avoided.
Right. I understand the difference between static and dynamic dispatch. But SE-335 reads like this to me: “Not only do existential any types pay the performance cost of dynamic dispatch, they ALSO incur additional performance costs of pointer indirection and dynamic memory.”
Am I reading it wrong? I’m actually not sure what “dynamic” memory means—just a heap allocation? Or something even more expensive than the heap?
Leaving aside the specific example I’ve cited, I just want to understand the entire performance picture between subclasses and existential any protocols.
There's 3 main considerations around the performance of existential containers:
The cost of dynamically dispatching methods (as opposed to statically dispatching)
The cost of adding retains/releases for those ARC-managed boxes
The cost of heap-allocation/boxing
Existentials are pretty similar to using classes in all 3 cases:
If you decided you need runtime polymorphism, then you need to pay the cost of dynamic dispatch, one way or another. This is equal between classes and existentials.
Existentials and class instances are both heap-allocated and managed by ARC, so there's no difference there.
This is where the main difference comes from.
A class instance is allocated on the heap once, and pretty much stays there. So you pay the allocation cost only once for the lifetime of that object.
With protocol existentials, there's a risk that you shuttle it back/forth between an inlined-allocated value and a heap-allocated box. Each time you do that, you incur a brand new allocation/deallocation, so you pay the cost multiple times. That usually happens inadvertently, as a value gets passed through many layers of a system. Here's a contrived example of that:
let i1 = 123
let box1: Any = i1 // First allocation
let i2 = box1 as! Int // Unbox, throw away box1
let box2: Any = i2 // Second allocation
let i3 = box2 as! Int // Unbox, throw away box2
let box3: Any = i3 // Third allocation
An interesting way to think of it is that object instances are their own box. They each start with a header that points to their class, which itself has all the necessary metadata (the vtable of methods, the size of the object), etc. Whereas for struct/enum values, boxes are created upon request whenever they needs to be used as an any type.
In any case, start with whatever is most ergonomic for your problem, and measure/optimize from there.
I probably will oversimplify and might be wrong here, but my understanding of these things is the following.
The difference only lies in using concrete type versus "dynamic", with the latter can be either an existential or an parent-child class hierarchy.
When you have concrete type – say static dispatch – the size, layout, all these low-level things are clear for the compiler as it has the exact type to deal with. Situation with existentials is opposite: nobody knows what type is hidden behind the existential, what size it will require, because the actual type will be put there at runtime. To address this, existential needs to be boxed – if you have an experience of writing type erasures, this is pretty much it. Type erasure resorts to dynamic dispatch in order to give you an ability to use any type that conforms to the protocol, but at the cost of dynamic allocations.
With classes and inheritance you already have dynamic allocations, witness table, and all these sort of things as well, so from the perspective of compiled code there is almost no difference. The citation from the SE refers to the difference with static dispatch, which is not the case for subclassing.
@AlexanderM has better cover above for what I'm saying
I’d end up with a lot of extra unboxing overhead, then, because protocols can’t be used as collection element types. They have to be type-erased to something like AnyHashable and then, when retrieved from the collection, unboxed with as! Persistable.
It sounds like that process involves more allocations and ARC traffic than just using an abstract superclass as the element type.
any types can be used as collection element types for…two? three? releases now, so that's no longer a consideration.
Downcasting, however, is more expensive for protocol types than classes. Since class inheritance is linear, the runtime can simply look at the dynamic type of the object and walk up the chain of superclasses until it finds the target type or not. But protocols are not just a list on every class; they can be added after the fact, and so the runtime (naively) has to do a search for the specific (class, protocol) pair in every loaded library. And generics can make this even more complicated. There are optimizations to improve the situation, but it is a difference.
Of course, there are benefits to using protocols; in general they're more flexible, especially when there might be an existing class hierarchy involved, and they also allow for associated types. But if you are strictly comparing for performance, a base class gives the compiler and runtime more guarantees to work with, and so will usually win. (Though I can probably contrive some cases where the protocol will do better.)
At a low level, too, a protocol type, even a class-constrained one, takes more space than a base class, because overridable methods are stored directly on the class, but protocol requirements are stored in a separate table (the "witness table"). So the any value has both the object reference and a pointer to that witness table, being two pointers in size instead of just one.*
* @objc protocols are an exception here, since all @objc methods live in one big table on the class, combined at run time.
All that said, I highly suspect 95% of the time this won't be the operations that slow your program down or the memory usage that puts things over the limit. Pick the one that feels appropriate for modeling your business logic, and revisit if necessary.
protocol Persistable: AnyObject, Hashable, Equatable
{…}
// Nope. “any Persistable is not allowed. Only concrete types can conform to protocols.”
var things: Set<Persistable> = []
I’m summarizing the compiler error because I’m away from Xcode at the moment and can’t recall the exact wording. The recommendation I found to deal with this was to erase to AnyHashable for the element type.
The Equatable problem I understand. Equatable requires two objects of the same concrete type and the protocol doesn’t guarantee that—we could be comparing a Foo and a Bar, both of which are Persistable but each of which is a distinct concrete type.
The Hashable issue is less clear to me. Seems like it shouldn’t matter what the concrete type is; all we need is a hash and the protocol guarantees that the thing we need to hash will respond to the hash(into:) call.
Bleah, right, they can go in collections but not collections that guarantee uniqueness. It is indeed Equatable that’s the problem. I think it would be possible to build an AnyHashable that isn’t quite so erasing either (though AnyHashable specifically has some extra logic to try to paper over bridging differences), but that’s a lot more work, so, point taken. A base class makes that much easier to deal with.
Existentials incurring these additional performance costs is only really a concern with instances that have a size over three words. (A word is the size of a pointer — eight bytes on a 64-bit machines.) This is because of how existentials are stored. Most existentials are stored like this:
The first three words are used to store the instance that has been cast into the existential. If the instance has a size of three words or less, this is easy — Swift just stores the instance in the buffer. If the instance is larger than three words, though, you can't store the entire instance directly in the buffer. So, instead, Swift will allocate memory on the heap for the instance, and then it will store the pointer in the inline buffer. This heap-allocated memory is reference-counted and uses copy-on-write.
The protocol witness table is a table that maps a protocol's requirements to a conforming type's members. This table is used for dynamic dispatch whenever one of the protocol's requirements are accessed from the existential.
The value witness table is a table that stores how operations like copy and destroy are implemented for a certain type.
Since class instances are stored as a pointer, they always have a size of only one word. So you don't need to worry about any additional allocations.
In fact, if an existential is constrained to AnyObject (e.g. any Persistable in your example), then the existential's layout is this:
Because class instances are always one word, the existential only needs one word for the inline buffer. And because class instances are all copied and destroyed in the same way (using ARC), there's no need to pass around the value witness table.
Contrast this to how overrides in subclasses work — when you call an overridden method on a class instance, Swift dereferences the class instance's pointer to get the isa pointer, and then it dereferences that pointer in order to look up the method in the class's vtable. This mechanism allows class instances to have a smaller size than class-constrained existentials (one word instead of two), but it now needs two dereferences for method lookup instead of one. Moving around the extra word is often cheaper than doing the extra dereferences.
There are two other special cases for existential layout that I know of:
any Any and any Sendable are stored like this:
3 words: inline buffer
1 word: value witness table pointer
There's no need for a protocol witness pointer because Any doesn't constrain any protocols and Sendable is a marker protocol.
any AnyObject and any AnyObject & Sendable are stored like this:
1 word: inline buffer
There's also no need for a protocol witness pointer here either since AnyObject also doesn't constrain any protocols.
(Note: the exact details of how existentials work aren't well-documented. I've tried to be as accurate as possible, but there may be some details I've gotten wrong. Even if everything's correct, it's possible that there will be changes to how existentials work in the future.)
You can work around this limitation with a wrapper. This code should work:
import Foundation
protocol Persistable: AnyObject, Hashable, Equatable {
var id: UUID { get set }
func save()
}
struct HashablePersistable {
var wrapped: any Persistable
init(_ wrapped: any Persistable) {
self.wrapped = wrapped
}
}
extension HashablePersistable: Hashable {
static func == (lhs: Self, rhs: Self) -> Bool {
return AnyHashable(lhs.wrapped) == AnyHashable(rhs.wrapped)
}
func hash(into hasher: inout Hasher) {
hasher.combine(AnyHashable(wrapped))
}
}
var things: Set<HashablePersistable> = []
When you want to put an any Persistable into the set, just wrap it in a HashablePersistable first. When you want to get an any Persistable from the set, unwrap it by getting the wrapped property.
I think the compiler should be able to avoid unnecessary ARC traffic here, but I'm not sure. Someone with a better understanding of the compiler's internals would be able to give a better answer.
Just a small side note: SwiftData doesn't use class inheritance (IIRC it doesn't even support it for subentities). The SwiftData PersistentModel is an AnyObject protocol, not a base class, in the context of the original question (and the ModelContext has quite a few [any PersistentModel]'s).
In fact Any is just the empty protocol composition, so all of these are special cases of a general principle:
An existential type is a protocol composition that might contain a superclass bound or AnyObject, together with zero or more protocols.
If the composition contains a superclass bound or AnyObject, or at least one of the protocols in the composition is class-constrained, then the payload is a single reference counted pointer. Otherwise the payload is three words together with type metadata. If the size of the type exceeds three words or has non-standard alignment, the first word of the payload is a pointer to reference-counted box, and the other two words are unused; otherwise, the value is stored inline.
For each non-@objc, non-@_marker, protocol member in the composition, also add a witness table.
Abstraction over the size and layout of a value. A value of class type is always a single reference-counted pointer, whereas an existential of a (non-class-constrained) protocol type can store values of varying sizes and alignments. So to move, copy, or destroy an existential requires an additional indirection that is not needed when you move, copy, or destroy a value of class type.
SwiftData doesn't use class inheritance (IIRC it doesn't even support it for subentities). The SwiftData PersistentModel is an AnyObject protocol, not a base class, in the context of the original question (and the ModelContext has quite a few [any PersistentModel] 's).
Interesting.
Do you know how that ultimately glues into Core Data, which expects to work with NSManagedObject subclasses? (I had heard that Swift Data is a thin wrapper on top of Core Data, but I haven’t looked at it closely.)
Unfortunately, Core Data (and therefore Swift Data) is useless in a wide swath of modern apps because sync is tied to an iCloud account and the data can’t be accessed or used outside of Apple’s ecosystem. They invested a lot of time polishing a framework built for the world as it was 15 years ago instead of creating a framework for the world as it is now (i.e. Apple should have just bought Realm, deprecated Core Data, and had a much more flexible persistence story.)
This specific thing is off-topic for this thread, maybe it should be moved elsewhere.
SwiftData is a pretty thick wrapper around Core Data and since the last rev even that (CD) can be replaced. Wrappers around wrappers around wrappers For a thin "wrapper" you could checkout ManagedModels.
The SwiftData model objects are distinct objects from the Core Data NSManagedObject's backing them (and I assume it is just one class for all entities, aka EOGenericRecord, but not sure). Those are wrapped and hidden in something called the BackingData (expand the @Model macro to see more about the innards).
Fortunately, quite a while ago, Apple also introduced a tech to make it easier to use iCloud sync w/o Core Data/SwiftData (though CloudKit always worked standalone as well): CKSyncEngine | Apple Developer Documentation.
It's pretty nice and allows you to integrate iCloud sync w/ your non-Core Data storage.
Either way, to bring it a little back on-topic: SwiftData is using a protocol, not a base class, as the abstraction of choice. And I'd personally recommend the same (even if you need objects).
I guess if you mean there’s no built in way to sync with anything else, but there’s no reason you or a third-party couldn’t write a library to sync with whatever you want.