Understanding Copy-on-Write in Swift
Swift is a value-type-heavy language. Structs, enums, arrays, dictionaries — they’re all value types. Pass them around and you get a copy every time, right?
Not quite. If Swift actually copied a 10,000-element array every time you passed it to a function, your app would grind to a halt. The trick is Copy-on-Write — the runtime delays the copy until the moment someone actually mutates the data.
Here’s how it works, why it matters, and how to implement it yourself.
The Problem: Value Semantics Are Expensive
Value semantics are great: no shared mutable state, no race conditions, no surprises when a distant piece of code changes your data. But they come with a price tag. Given this:
var a = Array(1...10_000)
var b = a // Copy 10k elements?
Without any optimization, b would need to allocate new storage and copy every element. For small arrays this is fine. For large ones it’s unacceptable.
The Solution: Share Until You Can’t
The key insight: if nobody mutates the data, multiple variables can safely share the same underlying storage. Only when someone writes do you fork the buffer.
var a = [1, 2, 3, 4, 5]
var b = a ─── Shared buffer, no copy yet
b.append(6) ─── Fork! b gets its own buffer
Under the hood, Swift’s standard library collections (Array, Dictionary, Set, String) manage this with a reference-counted backing buffer. The struct itself is small — just a pointer and some bookkeeping. The actual data lives in a heap-allocated buffer with a reference count.
._buffer
._count"] b["Array#laquo;T#raquo;
._buffer
._count"] end space block:Heap:1 c["Backing Buffer\nrefCount: 2\n[1,2,3,4,5]"] end a --> c b --> c
When b.append(6) runs, the runtime checks isKnownUniquelyReferenced(&buffer). It sees refCount == 2, knows the buffer is shared, allocates a new one, copies the data, and decrements the old refcount. Only b’s buffer changes. a is untouched.
The Magic Function: isKnownUniquelyReferenced
This function is the heart of CoW. It checks whether an object has exactly one strong reference pointing to it. If it does, the mutation can happen in place — no copy needed.
mutating func append(_ newElement: Element) {
if !isKnownUniquelyReferenced(&_buffer) {
_buffer = _buffer.copy()
}
_buffer.append(newElement)
}
This is (roughly) what Array’s append does. The check is cheap — just a reference count comparison. The copy only happens when it has to.
Which Types Use CoW?
Not all Swift types do. The standard library collections that use it:
| Type | CoW? | Notes |
|---|---|---|
Array | Yes | Backed by a shared buffer |
Dictionary | Yes | Same mechanism |
Set | Yes | Same mechanism |
String | Yes | But more complex — supports small-string optimization |
Data | Yes | Foundation type |
Substring | No | Shares the parent String’s buffer directly |
| Your struct | No | Unless you implement it |
Regular structs with stored properties do not get automatic CoW. Each property is copied eagerly. If you have a large struct and pass it around, you pay the full copy cost every time — which is why wrapping it in a CoW pattern matters for large data.
Performance: When It Helps and When It Hurts
CoW shines when reads far outnumber writes. Passing a large array through a chain of read-only functions? Nearly free — just reference count bumps. But if every call site appends to the array, you’ll copy every time and pay the same price (plus the CoW check overhead).
Cost = (reads × 0) + (writes × copy_cost) + (mutations × check_overhead)
The check itself (isKnownUniquelyReferenced) is essentially free. The copy is the expensive part, and it only happens when there’s contention — someone else is also holding a reference and you want to mutate.
Connections to ARC
CoW and Automatic Reference Counting are deeply intertwined. The refcount that isKnownUniquelyReferenced checks against is the same refcount ARC manages. Every time you pass a class instance around, ARC bumps it. CoW piggybacks on that infrastructure.
This also means CoW inherits ARC’s threading behaviour: reference count operations are atomic, so reads are thread-safe, but CoW itself doesn’t make your type thread-safe. Two threads mutating the same array simultaneously can still cause a data race on the CoW check — one thread might copy while the other mutates in place, leading to lost updates. Always serialize writes.
Summary
- CoW delays copies until a mutation forces a fork.
- Array, Dictionary, Set, String, and Data all use it.
isKnownUniquelyReferencedis the enabler — cheap, fast, and class-only.- Custom CoW types are straightforward but watch out for accidental extra references.
- Reads are free, writes pay the copy cost only when the buffer is shared.
- CoW doesn’t replace thread safety — still need synchronization for concurrent writes.