Copy-on-write and value-type performance

Swift · Expert · 16 min

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 with dict[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 with some P avoids this.
  • Non-final classes and protocol requirements use dynamic dispatch; final, private and whole-module optimization enable devirtualization.
  • @inlinable exposes 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 copy

Remember

  • CoW copies a shared buffer only on mutation, after a uniqueness check
  • Mutate in place; temporary copies defeat CoW
  • Use final, generics and -O builds when measuring performance

Watch out for

  • Benchmarking in Debug builds, where copies and dispatch are not optimized away.

Check yourself

  1. `var a = [1, 2, 3]; var b = a; b[0] = 9` — when are the elements actually copied?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: github.com/swiftlang/swift/blob/main/docs/OptimizationTips.rst