One of the major changes in Swift 4.2 is a change to the calling convention. But what exactly does that mean? Why is it important and why would you want to change it?
The calling convention specifies how subroutines (e.g., functions) should receive their arguments, the order of those arguments, and how they should return a result. It also specifies how to setup the registers and the call stack when calling a subroutine — where arguments are stored, how return values are passed back, and how to restore the state of the registers and memory when the function returns. There is plenty more to consider here, but this gives you the general idea of what a calling convention defines. All of this requires a specific contract between the caller and the callee, so that each knows which instructions it is responsible for executing. This is a key component to ensuring programs execute correctly.
At a higher level, the calling convention defines if and when parameters are pass-by-reference or pass-by-value. It also defines the ownership conventions about parameter values — in the case of Swift, who is responsible for retaining and releasing them, and when? The caller or the callee? This is what changed in Swift 4.2.
An aside: there’s an in-depth Calling Convention doc in the main Swift that explores this topic in detail. While it has not been updated for 2 years, much of the information is still relevant. Be aware that there may be some errors in any sections that attempt to describe Swift’s current behavior.
What changed in Swift 4.2?
Swift uses a reference-counted memory model and provides automatic memory management via ARC like Objective-C. This means these changes to calling convention in Swift are totally hidden from the user, since the compiler inserts the calls needed to retain and release objects.
In the talk, Ted provides the following examples to illustrate the calling convention prior to Swift 4.2, “callee-owned”. When an object is created, it has a +1 reference count. However, when passed to a function call, it’s the obligation of that function to release the object.
In practice, we can see how this can produce a lot of superfluous, wasteful retain and release calls. (Note that the initial reference count is balanced by the final function call.)
In Swift 4.2, the convention has changed to “Guaranteed”.
It is no longer the callee’s responsibility to release the object. So, all of the superfluous retains and releases go away. Ted notes in the talk that this significant reduction in retain and release traffic results in a code size reduction as well as a runtime performance improvement, because all those calls have been removed.
Calling non-inlinable functions in other modules
What’s more interesting with this change is the case of calling non-inlinable functions across module boundaries. Michael Gottesman shared this gist on Twitter to explain. I doubt many people saw that Tweet, so I wanted to highlight it. I’ve reproduced the gist below:
That’s an edge case to which I hadn’t given much thought. In the old convention (“callee-owned”), it was not possible to perform this retain/release traffic optimization across modules — there wasn’t enough information, not to mention everyone needs to follow the convention. The result was slower code. But now that the retains and releases are the responsibility of the caller, not the callee, the optimizer can eliminate all of the retain/release traffic to produce optimal code.
I love reading about these kinds of performance improvements, and there are many in Swift 4.2.