Knowing what a value copy really costs is the difference between elegant and slow Swift.
Standard collections are copy-on-write (CoW): copying an Array just copies a reference to a shared buffer and bumps its count. The actual copy happens on the first mutation of a shared buffer, which checks isKnownUniquelyReferenced. If the buffer is unique, it mutates in place.
You can give your own value types this behavior by storing data in a private class and copying it before mutation when it's not unique:
if !isKnownUniquelyReferenced(&storage) { storage = storage.copy() }Common performance traps:
- Accidental copies: holding a second reference while mutating (e.g.
let old = array; array.append(x)) forces a full copy. Mutating through an intermediate variable copy,var tmp = dict[key]!; tmp.append(x); dict[key] = tmp, copies too; mutate in place withdict[key, default: []].append(x). - Large structs with many class-typed fields cost a retain/release per field per copy. Grouping fields into one CoW box helps.
- Existentials (
any P) store values inline only up to three words; larger ones are boxed on the heap. Generic code withsome Pavoids this. - Non-final classes and protocol requirements use dynamic dispatch;
final,privateand whole-module optimization enable devirtualization. @inlinableexposes a function body across modules so generics can be specialized for callers.
Measure with Instruments (Time Profiler, Allocations) and build with -O; debug builds are unrepresentative.
Example
final class Storage {
var values: [Int]
init(_ v: [Int]) { values = v }
}
struct Vector {
private var storage: Storage
init(_ v: [Int]) { storage = Storage(v) }
var values: [Int] { storage.values }
mutating func append(_ x: Int) {
if !isKnownUniquelyReferenced(&storage) {
print("copying")
storage = Storage(storage.values)
}
storage.values.append(x)
}
}
var a = Vector([1, 2])
a.append(3) // unique: no copy
var b = a // shares storage
b.append(4) // prints "copying"
print(a.values, b.values) // [1, 2, 3] [1, 2, 3, 4]
var groups: [String: [Int]] = [:]
groups["x", default: []].append(1) // in-place, no temporary copyRemember
- CoW copies a shared buffer only on mutation, after a uniqueness check
- Mutate in place; temporary copies defeat CoW
- Use
final, generics and-Obuilds when measuring performance
Watch out for
- Benchmarking in Debug builds, where copies and dispatch are not optimized away.