Is existential any a performance problem in Swift?

Swift 5.6 was released in March with Xcode 13.3 and among other changes it introduces a new keyword – any. The Swift evolution proposal for it is SE-0335: existential any. Existential types are used for storing an any kind of value which conforms to a specific protocol. Therefore, whenever we use protocol for defining a property type or function argument, then we are encountering existential types. In Swift 5.6, using the any keyword is optional, but it is expected to be required in Swift 6.

protocol Computable {
func compute() -> Int
}
struct DataCore {
let dataProcessors: Computable
}
// becomes
struct Core {
let dataProcessor: any Computable
}

The evolution proposal’s motivation paragraph mentions that “Existential types in Swift have significant limitations and performance implications.” (SE-0335) and “Existential types are also significantly more expensive than using concrete types.” (SE-0335). Which can lead to thinking that one should try to avoid existential types, meaning protocols, because of performance implications. One might even ask if we should always try to use generics over protocols?

// Should I use this?
struct Core {
let dataProcessor: any Computable
}
// or should I inject dependencies using generics?
struct Core2<T: Computable> {
let dataProcessor: T
}

My answer is to continue using protocols as we have so far. Yes, the protocol based approach needs a tiny bit more CPU cycles as Swift needs to do dynamic dispatch and look up the exact type which conforms to the protocol in runtime, but dynamic dispatch is nothing new. Objective-C was all about dynamic dispatch, and we rarely needed to think about it, only when dealing with performance critical code it could have showed up. Therefore, when reading about existential any it is possible that we get a feeling like we should, because of performance, replace everything, what is possible, with generics. But that should not be a case. Performance becomes relevant only in areas where we need to call a loooot of functions in a very short time. As a silly micro benchmark, I set up a code which created a million structs which require more than 3 word buffer (makes Swift to use heap memory) and then calling a function on these values. I was seeing that 2 ms of CPU time was spent in “__swift_project_boxed_opaque_existential_1”. If I then compared it with generics based implementation, then it was 3 ms faster (7 ms vs 4 ms). But to reiterate, a million values were involved. In summary, there is no need to go and refactor old code which uses existential any and replace it with generics only because of generics being faster. Both have their own use-cases.

A snapshot of the generics based implementation in Time Profiler.
A snapshot of the existential any based implementation in Time Profiler.

If this was helpful, please let me know on Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

1 Comment »