I’ve been prototyping a new feature aimed at performance-conscious programmers that removes the mystery around when copies (such as retains) of values occur in their code. The idea is that you can ask the compiler to force you to acknowledge every use of a variable binding that requires a copy of it. Let’s take a look at an example:
class Data {
var value: Int = 0
}
func accumulate(_ xs: [Data]) -> Data {
let total = Data()
for x in xs {
total.value += x.value
}
return total
}
If this accumulate function were on an ultra-hot path of code, I’d want to know exactly where all copies might be happening, so that I can tweak the code to minimize them. When I turn on explicit copies mode for this function, I’d get diagnostics such as this first one:
forum-examples.swift:7:12: error: explicit 'copy' required here
5 | func accumulate(_ xs: [Data]) -> Data {
6 | let total = Data()
7 | for x in xs {
| `- error: explicit 'copy' required here
8 | total.value += x.value
9 | }
Here, it’s pointing out that when iterating over my array xs
, a copy of it is made. As a programmer, I have two options: (1) acknowledge the copy by writing for x in copy xs
, or (2) I could push that copy out of this function and into callers by taking xs
as a consuming
parameter (in hopes that it’s the last-use within callers to elide the copy). If you’re curious why there’s a copy when iterating over a collection at all, it’s because Collection’s makeIterator
is a consuming method, and that’s implicitly invoked at the start of this for-in loop to begin the iteration.
The next place where there’s a copy is when returning the result:
forum-examples.swift:10:10: error: explicit 'copy' required here
8 | total.value += x.value
9 | }
10 | return total
| `- error: explicit 'copy' required here
11 | }
12 |
In terms of language semantics, the return
of an lvalue expression like total
does make a copy of it. But in this particular case, the copy is automatically elided, even in -Onone
(aka no optimizations).
Sometimes, copies are not always elided in all optimization modes. For example, this copy of x
is not elided in -Onone
:
func elision(_ cond: Bool) -> Data {
let x = Data()
let y = copy x // <- not elided in -Onone
if cond {
return consume y
}
return consume x
}
So, there are a few areas of the design that’s yet to be hammered out, such as:
- What kinds of copies should be acknowledged, and how (warnings, errors)?
- I personally don’t think your program’s diagnostics should change based on the optimization level (e.g., Debug vs Release), so all copies that are not guaranteed to eliminated must be acknowledged.
- How should the boundaries of this explicit copy mode be defined?
- It’s currently prototyped as a function-level attribute that is non-viral: it doesn’t require callers or callees to acknowledge copies in their respective functions. It’s likely people will want file-level and/or module-level application of this mode. But there’s functions that are marked
@_transparent
that are always inlined, such as synthesized getters, which blurs that boundary.
- It’s currently prototyped as a function-level attribute that is non-viral: it doesn’t require callers or callees to acknowledge copies in their respective functions. It’s likely people will want file-level and/or module-level application of this mode. But there’s functions that are marked
Happy to hear people’s thoughts on this!