Changing class convenience initializers to perform whole object allocation and @objc interop

In Swift's implementation today, all class initializers are split into "allocating" and "initializing" entry points. The allocating entry point is what gets called when you write an initializer call like ClassName() in Swift; it allocates the object, and passes the uninitialized memory buffer to the initializing entry point to perform the initialization. The initializing entry point corresponds to what you write in an init() { ... } body, assigning initial values to all stored properties and performing other necessary setup.

This split resembles the +alloc/-init convention in Objective-C, and C++ implementations split initializers in similar ways. It is fundamentally necessary for designated initializers, since a designated initializer for a subclass must be able to allocate an instance of the subclass and then chain to a designated initializer of the base class to perform the initialization of only the base part. It is however a constraining and inefficient design for convenience initializers, which always delegate to another peer initializer to perform the entire object initialization. The initializer that a convenience initializer delegates to could be a protocol extension initializer, a C or Objective-C factory function imported as an initializer, or (in the fullness of time) a Swift factory initializer, and in these situations, the convenience initializer must throw away the allocated object it was given so that the delegated initializer can allocate a new one. It would be a better pure Swift ABI for convenience initializers to have only the "allocating" entry point, and leave allocation as the responsibility of the initializer they ultimately delegate to.

However, Swift initializers can still be exported as @objc for Cocoa interop, and in that case, we must still present a +alloc/-init split for consistency with Objective-C conventions. If we change the ABI for Swift convenience initializers to always perform allocation, then there could be a performance hit for the Objective-C -init interfaces to these initializers, since they will always have to deallocate and reallocate a new object, whereas that is currently avoided as long as the Swift convenience initializer eventually delegates to a designated initializer. We have a few options here:

  • If an initializer is @objc, keep the existing ABI. This would be unfortunate, since up until now, @objc in classes has not affected Swift ABI and can be freely added without breaking Swift binary compatibility.
  • If an initializer is @objc, have the public ABI be the allocating entry point, but keep an internal initializing entry point. This would allow @objc thunks within a resilience domain to avoid the reallocation. If an initializer delegates to another convenience initializer in a different module, it would still have to reallocate.
  • Accept the performance hit for @objc interfaces.

Any alternatives I missed? My gut feeling is that class instance allocation is not generally something you want in a hot loop to begin with, but I wanted to get feedback before committing to an approach. Thanks!

3 Likes

There's another rather ridiculous alternative as well: override +allocWithZone: to return a dummy object (no allocation), and then check in designated initializers' ObjC entry points whether we're being called on that dummy object.

3 Likes

Changing the ABI now provides the most flexibility for future performance. It seems to me the potential ObjC overhead can be handled on the ObjC side, at least within a resilience domain.

Do we know how big the impact on UIKit and Foundation and code that uses/subclasses either is?

I agree that we shouldn't allow @objc to affect the ABI.

The perf hit would just be for calling a Swift-implemented convenience initializer from Objective-C? I think we can live with that.

3 Likes