I’ve been mulling this over for a while, and I think there is an opportunity to make efficient code as ergonomic as inefficient code, when it comes to reusing existing buffer storage for the result of a calculation.
Suppose you have a type which uses a buffer for storage, and certain operations return that type. For example, a Vector
or Matrix
with arithmetic operators.
Code like this will of course allocate a new instance with a new buffer for w
on each iteration:
for u in vecList {
for v in vecList {
let w = u .- v
// use w
}
}
Unfortunately so will code like this:
var w = Vector()
for u in vecList {
for v in vecList {
w = u .- v
// use w
}
}
Even if the result of a calculation is stored to an instance which already exists and has a buffer capable of holding the result, that existing buffer is not used. In this example, the subtraction operator creates a new instance of Vector
, which allocates a new buffer to hold the result, regardless of the fact that w
already had a perfectly serviceable buffer right there.
In order to reuse the storage buffer in w
, we have to write code like this:
var w = Vector()
for u in vecList {
for v in vecList {
u.minus(v, storedInto: &w)
// use w
}
}
That is unwieldy, and I think we can all agree it is a pattern we do not want to encourage.
If instead it were possible to make the return value of the operator use existing storage when available, then we could keep the natural spelling for the assignment, namely w = u .- v
, and still get the efficiency of reusing the same buffer.
Perhaps the declaration of .-
might look something like this:
extension Vector where Element: Numeric {
static func .- (lhs: Self, rhs: Self) -> (result: inout Self = Vector()) {
let n = lhs.storage.count
precondition(rhs.storage.count == n)
result.storage.removeAll(keepingCapacity: true)
result.storage.reserveCapacity(n)
for (x, y) in zip(lhs.storage, rhs.storage) {
result.storage.append(x - y)
}
}
}
The declaration syntax is entirely bikesheddable, but the conceptual idea is that the instance into which the return value will be stored (w
), is available as a mutable parameter inside the function body.
The default value for result
indicates that if no existing instance is provided (such as in let w = u .- v
) then the default is used. If no default were provided, then presumably there would be a separate declaration of the operation with a standard, non-mutable return value for those calls.
Note that this is different from what I understand previous discussions about “inout return values” to mean. I am not tied to any particular spelling or name for the feature I am describing, but I think it is valuable.