Swift uses reference counting and copy-on-write to allow developers to
write code with value semantics and not normally worry too much
about performance or memory management. However, in performance sensitive code,
developers want to be able to control the uniqueness of COW data structures and
reduce retain/release calls in a way that is future-proof against changes to
the language implementation or source code. Consider the following
example:
func test() {
var x: [Int] = getArray()
// x is appended to. After this point, we know that x is unique. We want to
// preserve that property.
x.append(5)
// Pass the current value of x off to another function, that could take
// advantage of its uniqueness to efficiently mutate it further.
doStuffUniquely(with: x)
// Reset x to a new value. Since we don't use the old value anymore,
// it could've been uniquely referenced by the callee.
x = []
doMoreStuff(with: &x)
}
func doStuffUniquely(with value: [Int]) {
// If we received the last remaining reference to `value`, we'd like
// to be able to efficiently update it without incurring more copies.
var newValue = value
newValue.append(42)
process(newValue)
}
In the example above, a value is built up in the variable x
and then
handed off to doStuffUniquely(with:)
, which makes further modifications to
the value it receives. x
is then set to a new value. It should be possible for
the caller to forward ownership of the value of x
to doStuffUniquely
,
since it no longer uses the value as is, to avoid unnecessary retains or
releases of the array buffer and unnecessary copy-on-writes of the array
contents. doStuffUniquely
should in turn be able to move its parameter into
a local mutable variable and modify the unique buffer in place. The compiler
could make these optimizations automatically, but a number of analyses have to
align to get the optimal result. The programmer may want to guarantee that this
series of optimizations occurs, and receive diagnostics if they wrote the code
in a way that would interfere with these optimizations being possible.
Swift-evolution pitch threads:
Proposed solution: take
operator
That is where the take
operator comes into play. take
consumes
the current value of a binding with static lifetime, which is either
an unescaped local let
, unescaped local var
, or function parameter, with
no property wrappers or get/set/read/modify/etc. accessors applied. It then
provides a compiler guarantee that the current value will
be unable to be used again locally. If such a use occurs, the compiler will
emit an error diagnostic. We can modify the previous example to use take
to
explicitly end the lifetime of x
's current value when we pass it off to
doStuffUniquely(with:)
:
func test() {
var x: [Int] = getArray()
// x is appended to. After this point, we know that x is unique. We want to
// preserve that property.
x.append(5)
// Pass the current value of x off to another function, that
doStuffUniquely(with: take x)
// Reset x to a new value. Since we don't use the old value anymore,
x = []
doMoreStuff(with: &x)
}
The take x
operator syntax deliberately mirrors the
proposed ownership modifier
parameter syntax, (x: take T)
, because the caller-side behavior of take
operator is analogous to a calleeās behavior receiving a take
parameter.
doStuffUniquely(with:)
can use the take
operator, combined with
the take
parameter modifier, to preserve the uniqueness of the parameter
as it moves it into its own local variable for mutation:
func doStuffUniquely(with value: take [Int]) {
// If we received the last remaining reference to `value`, we'd like
// to be able to efficiently update it without incurring more copies.
var newValue = take value
newValue.append(42)
process(newValue)
}
This takes the guesswork out of the optimizations discussed above: in the
test
function, the final value of x
before reassignment is explicitly
handed off to doStuffUniquely(with:)
, ensuring that the callee receives
unique ownership of the value at that time, and that the caller can't
use the old value again. Inside doStuffUniquely(with:)
, the lifetime of the
immutable value
parameter is ended to initialize the local variable newValue
,
ensuring that the assignment doesn't cause a copy.
Furthermore, if a future maintainer modifies the code in a way that breaks
this transfer of ownership chain, then the compiler will raise an
error. For instance, if a maintainer later introduces an additional use of
x
after it's taken, but before it's reassigned, they will see an error:
func test() {
var x: [Int] = getArray()
x.append(5)
doStuffUniquely(with: take x)
// ERROR: x used after being taken from
doStuffInvalidly(with: x)
x = []
doMoreStuff(with: &x)
}
Likewise, if the maintainer tries to access the original value
parameter inside
of doStuffUniquely
after being taken to initialize newValue
, they will
get an error:
func doStuffUniquely(with value: take [Int]) {
// If we received the last remaining reference to `value`, we'd like
// to be able to efficiently update it without incurring more copies.
var newValue = take value
newValue.append(42)
process(newValue)
}