A roadmap for improving Swift performance predictability: ARC improvements and ownership control

We may investigate this more when the concurrency model has been better fleshed out. Some of the features planned to enforce data race safety may allow us to know when certain references are thread-isolated and can use nonatomic refcounting.

Working with the weak reference itself shouldn't change much under this proposal. If you're going to perform multiple operations on the same object, you'll still want to "strongify" the reference to ensure you have a stable reference for the duration of those operations. But if let and guard let in Swift already do that for you. Lexical lifetimes intend to make the side that holds the one strong reference to the object less hazardous, by making it less likely you need to use withExtendedLifetime to stop the optimizer from shortening the lifetime of the strong reference.

Rust has "non-lexical lifetimes" too, though it's stricter than Swift's traditional rule, since the end of a binding's lifetime is pinned to its last use in source, whereas Swift didn't consider no-op loads like _ = x to be formal uses of x, and so inlining and other optimizations could allow lifetimes to shorten even further than the last source-level use of the variable. Rust's non-lexical lifetime rule could however still pose a hazard for weak references in the example from the original post, since the last source-level use of the strong reference to the delegate object still happens before the access into the weak reference.

In this particular example, there would be no change from today, since the initializer contains no deinitialization barriers. Both today and with lexical lifetimes, you will likely see an extra copy on write in debug builds, because people generally expect local variables to be readable anywhere in scope in the debugger, and maybe that copy will be elided in optimized builds, but there's no strong guarantee either way. move provides a way to assert an important performance property, but it still isn't strictly necessary to get the benefit of optimization.

I would say that async is different from other coroutines, because the coroutine transform for async functions is really an implementation detail; they otherwise behave like regular functions at the surface level. The ownership manifesto does discuss supporting generator coroutines in the future, which would allow for more efficient for loops that borrow their underlying sequence's storage, similar to how accessor coroutines allow for more efficient property access than get/set functions.

An assignment is effectively destroying the old value in the LHS, and consuming the new value from the RHS. It would make sense to me if we still required an explicit copy or move if the RHS is a non-implicitly-copyable expression, and the assignment would necessitate a copy of the underlying value, so if you had:

@noImplicitCopy var x: ExpensiveType = expensiveValue()
var y: ExpensiveType, z: ExpensiveType
...
...
y = copy(x)
z = x

the copy on the assignment into y would be required at minimum, since expensiveValue() has to be copied to be able to assign into both y and z.

I think that, in order for it to be effective, @nonescaping will have to be viral to some degree like const, and it'll need to be retrofitted to a good chunk of the standard library in order for @nonescaping operations to be expressive. As part of the eventual proposal for @nonescaping, we shouuld discuss how we can manage that annotation burden.

7 Likes