Thursday, December 23, 2021

Roadmap for Improving Swift Performance Predictability

Joe Groff (via David Smith):

For these reasons, we think it makes sense to change the the language rules to follow what is most users’ intuition, while still giving us the flexibility to optimize in important cases. Rather than say that releases on variables can happen literally anywhere, we will say that releases are anchored to the end of the variable’s scope, and that operations such as accessing a weak reference, using pointers, or calling into external functions, act as deinitialization barriers that limit the optimizer’s ability to shorten variable lifetimes. The upcoming proposal will go into more detail about what exactly anchoring means, and what constitutes a barrier, but in our experiments, this model provides much more predictable behavior and greatly reduces the need for things like withExtendedLifetime in common usage patterns, without sacrificing much performance in optimized builds.


By making the transfer of ownership explicit with move, we can guarantee that the lifetime of the values argument is ended at the point we expect. If its lifetime can’t be ended at that point, because there are more uses of the variable later on in its scope, or because it’s not a local variable, then the compiler can raise errors explaining why. Since values is no longer active, self.values is the only reference remaining in this scope, and the sort method won’t trigger an unnecessary copy-on-write.


In practice, it follows some heuristic rules:

  • Most regular function arguments are borrowed.
  • Arguments to init are consumed, as is the newValue passed to a set operation.

The motivation for these rules is that initializers and setters are more likely to use their arguments to construct a value, or modify an existing value, so we want to allow initializers and setters to move their arguments into the result value without additional copies, retains, or releases. These rules are a good starting point, but we may want to override the default argument conventions to minimize ARC and copies. For instance, the append method on Array would also benefit from consuming its argument so that the new values can be forwarded into the data structure, and so would any other similar method that inserts a value into an existing data structure. We can add a new argument modifier to put the consuming convention in developer control[…]


A normal function stops executing once it’s returned, so normal function return values must have independent ownership from their arguments; a coroutine, on the other hand, keeps executing, and keeps its arguments alive, after yielding its result until the coroutine is resumed to completion. This allows for coroutines to provide access to their yielded values in-place without additional copies, so types can use them to implement custom logic for properties and subscripts without giving up the in-place mutation abilities of stored properties. These accessors are already implemented in the compiler under the internal names _read and _modify, and the standard library has experimented extensively with these features and found them very useful, allowing the standard collection types like Array, Dictionary, and Set to implement subscript operations that allow for efficient in-place mutation of their underlying data structures, without triggering unnecessary copy-on-write overhead when data structures are nested within one another.


When working with deep object graphs, it’s natural to want to assign a local variable to a property deeply nested within the graph[…] As above, we really want to make a local variable, that asserts exclusive access to the value being modified for the scope of the variable, allowing us to mutate it in-place without repeating the access sequence to get to it[…]

This looks great. A related improvement I’d like to see is a way to have function arguments that bypass ARC. For example, if I’m sorting a large array, it’s wasteful to have to retain and release the elements each time they’re passed to a comparator when they’re already owned by the array itself (which the comparator is not modifying). The same applies for iteration over a collection, or even a tree, that doesn’t change change its structure.


Update (2021-12-24): Joe Groff:

Your “pass array elements to a comparator function” example should already not require any r/r traffic today, since Array implements read accessors on its subscript, and functions borrow their arguments by default

So I’m not sure why I was seeing ARC overhead before.

1 Comment RSS · Twitter

> So I’m not sure why I was seeing ARC overhead before.

Maybe you did your tests before introduction of the 'read' accessor ?

Leave a Comment