Why does a mutation sometimes copy the whole array, and how do you prove it?
Predict first
What does this print?
var a = [1, 2, 3]
a.reserveCapacity(100)
let p1 = a.withUnsafeBufferPointer { $0.baseAddress }
a.append(4)
let p2 = a.withUnsafeBufferPointer { $0.baseAddress }
var b = a
a.append(5)
let p3 = a.withUnsafeBufferPointer { $0.baseAddress }
print(p1 == p2, p1 == p3)
_ = b
- true true
- true false
- false false
Mental model. Two people each “own” a page. The first to scribble forces a photocopy of the whole page, however small the scribble.
An array mutation copies only if its buffer is shared at that moment. The usual causes: another variable still holds the same array (you assigned it, or are iterating over it while mutating it); the array was passed by value into a function that then mutates its parameter, which has to copy because the caller still has it; a closure captured the variable; or a second structure (a cache, a snapshot) stores the same value. A related trap is array = array + [x], which builds a brand-new array every time instead of appending in place.
You can prove a copy happened by comparing the buffer’s base address before and after (withUnsafeBufferPointer), or by profiling for a memcpy in the mutation. Fix by sharing less: pass inout instead of by value, mutate in place with append or [key, default: []].append, and avoid holding a second reference during the loop. Reserve capacity up front so in-place growth does not reallocate.
With spare capacity and one owner, append works in place (same address). After var b = a the buffer is shared, so the next write moves a to a fresh copy.
Common traps
- Passing a big array by value into a mutating helper. The helper gets a shared reference and copies on its first write.
- Building a result with
result = result + [x]. Quadratic: every step allocates a new array.
They might follow up with
- What is
inoutdoing? It lets the callee mutate your storage directly, so no second owner exists. - How is a dictionary-of-arrays append done in place?
dict[key, default: []].append(x)mutates the stored array without copying it out first.
Related questions
- 3.5.2 What exactly does isKnownUniquelyReferenced check, and how do you defeat it by accident?
- 2.1.5 What happens if you mutate an array while you loop over it?
- 1.1.7 What is copy-on-write? Which types use it?
← What is an Array in memory, and what is special about the empty array?