Swift Copy-on-Write Optimization Explained
Copy-on-write is one of the most important performance features in Swift. It lets value types behave like independent values while delaying expensive copying until a mutation actually happens.
Quick answer: Copy-on-write means Swift can share storage between copied values until one of them changes. At that point, Swift makes a real copy so each value keeps its own data.
Difficulty: Intermediate
Helpful to know first: You'll understand this better if you know basic Swift value types, how copying assignment works, and the difference between classes and structs.
1. What Is Copy-on-Write?
Copy-on-write, often shortened to CoW, is an optimization strategy used by Swift value types such as Array, String, Dictionary, and Set. When you copy one of these values, Swift does not immediately duplicate all of the underlying data.
- The copied values start by sharing the same storage.
- When one copy needs to change, Swift creates a private copy first.
- Each value still behaves as if it owns its own data.
- This keeps common operations fast while preserving value semantics.
At the language level, this is why assigning an array to a new variable is usually cheap, even when the array contains many elements.
2. Why Copy-on-Write Matters
Without this optimization, every copy of a large value would require duplicating all of its contents immediately. That would make common Swift code much slower and use more memory than necessary.
Copy-on-write matters because it gives you the best of both worlds:
- Simple value semantics: You can pass and assign values without worrying that one part of your program will unexpectedly mutate another.
- Better performance: Copies stay cheap until a mutation happens.
- Lower memory use: Shared storage avoids unnecessary duplication.
This is especially valuable in code that passes arrays, strings, or dictionaries through many layers of a program.
3. Basic Syntax or Core Idea
There is no special syntax for using copy-on-write. You benefit from it automatically when working with Swift's standard value types. The core idea is easiest to see by assigning one value to another and then mutating one copy.
Simple assignment and mutation
The following example shows how a copied array can diverge after mutation:
var numbers = [1, 2, 3]
var moreNumbers = numbers
moreNumbers.append(4)
// numbers is still [1, 2, 3]
// moreNumbers is [1, 2, 3, 4]Before the append, both variables can share storage. The append requires mutation, so Swift makes sure moreNumbers gets its own copy first.
Why this is still value semantics
Even though storage may be shared behind the scenes, the observable behavior is that changing one variable does not change the other. That is the key promise of Swift value types.
4. Step-by-Step Examples
Example 1: Arrays share storage until mutation
This example shows the most familiar copy-on-write behavior in Swift. Assignment is cheap, but the first write after a shared copy triggers duplication.
var a = [10, 20, 30]
var b = a
b[0] = 99
// a is [10, 20, 30]
// b is [99, 20, 30]The mutation affects only b. Swift preserves independent results even though the values started out sharing data.
Example 2: String concatenation does not copy eagerly
String also uses copy-on-write. You can assign a string and change one copy later without affecting the other.
var message = "Hello"
var copy = message
copy += ", world"
// message is "Hello"
// copy is "Hello, world"This is important because strings can contain a lot of data. Swift avoids making them expensive to copy when there is no reason to do so.
Example 3: Dictionary updates follow the same rule
Dictionary values behave the same way. A copied dictionary stays cheap until one copy is modified.
var original = ["name": "Ava", "role": "Admin"]
var updated = original
updated["role"] = "Editor"
// original is ["name": "Ava", "role": "Admin"]
// updated is ["name": "Ava", "role": "Editor"]The important detail is that the mutation changes only the updated dictionary.
Example 4: A custom copy-on-write wrapper
You can build your own copy-on-write type when you need value semantics with internal sharing. Swift often uses a reference type as storage and a value type as the public wrapper.
final class BufferStorage {
var bytes: [UInt8]
init(bytes: [UInt8]) {
self.bytes = bytes
}
func copy() -> BufferStorage {
return BufferStorage(bytes: bytes)
}
}
struct Buffer {
private var storage: BufferStorage
init(bytes: [UInt8]) {
self.storage = BufferStorage(bytes: bytes)
}
mutating func append(_ byte: UInt8) {
if !isKnownUniquelyReferenced(&storage) {
storage = storage.copy()
}
storage.bytes.append(byte)
}
var bytes: [UInt8] {
storage.bytes
}
}
var first = Buffer(bytes: [1, 2])
var second = first
second.append(3)This pattern gives Buffer value semantics while postponing duplication until it is truly needed.
5. Practical Use Cases
Copy-on-write is useful any time you want values to be easy to pass around without paying a copy cost up front.
- Passing large arrays between functions while modifying only a few of them.
- Building text-processing code that creates many intermediate strings.
- Managing caches or collections in models where reads are far more common than writes.
- Designing custom value types that wrap mutable shared storage safely.
- Reducing memory traffic in performance-sensitive code that clones data often.
A good rule is: if the data is often copied but only occasionally mutated, copy-on-write can be a strong fit.
6. Common Mistakes
Mistake 1: Assuming every assignment makes a deep copy immediately
New Swift developers often think let b = a duplicates all data right away. In reality, Swift often delays the copy until mutation.
Problem: Expecting an eager deep copy can lead to wrong performance assumptions and confusing debugging when you inspect memory usage.
var numbers = [1, 2, 3]
let copy = numbers
// No full copy is required yet.Fix: Assume value semantics, not eager duplication. Swift will copy only if a mutation requires it.
var numbers = [1, 2, 3]
let copy = numbers
// The data stays shared until one side mutates.This mental model matches how Swift actually behaves and helps you reason about performance more accurately.
Mistake 2: Mutating through a copy and expecting the original to change
Because storage may start out shared, beginners sometimes expect both variables to reflect the change. That is not how value types work.
Problem: The mutation only affects the variable being modified, so the original remains unchanged.
var first = ["a", "b"]
var second = first
second.append("c")
// first is still ["a", "b"]Fix: If you want shared mutable state, use a reference type on purpose. If you want independent values, keep using Swift value types.
final class Box {
var items: [String] = []
}
let box1 = Box()
let box2 = box1
box2.items.append("c")The corrected version works because classes share identity, while arrays and strings preserve value behavior.
Mistake 3: Writing a custom type without protecting shared storage
If you build your own value wrapper around reference storage, you must check whether the storage is uniquely referenced before mutating it.
Problem: Mutating shared reference storage breaks value semantics because two copies can unexpectedly change together.
final class Storage {
var value: Int
init(value: Int) { self.value = value }
}
struct BrokenCounter {
var storage: Storage
mutating func increment() {
storage.value += 1
}
}Fix: Check uniqueness and copy before writing when storage is shared.
struct Counter {
private var storage: Storage
mutating func increment() {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(value: storage.value)
}
storage.value += 1
}
}This works because the copy happens before mutation, so each logical value remains independent.
7. Best Practices
Practice 1: Prefer value types for independent data
When different parts of your program should not affect each other, use struct, Array, String, or similar value types. Copy-on-write gives you safety without forcing expensive duplication up front.
struct Document {
var title: String
var tags: [String]
}This is a good fit because each document can be passed around independently.
Practice 2: Mutate only when necessary
Because the first mutation on shared storage may trigger a copy, it is better to group changes together instead of mutating the same copied value repeatedly in many places.
var items = [1, 2, 3]
var copy = items
copy.append(4)
copy.append(5)The first append may trigger the copy, but later appends reuse the new unique storage.
Practice 3: Use custom CoW only when profiling justifies it
It is easy to over-engineer a type with reference storage and uniqueness checks. Standard value types already use copy-on-write in many cases, so build your own only when you have a real need.
// Good reason: a custom buffer type with large shared storage.
// Weak reason: wrapping a tiny Int that never becomes expensive to copy.This keeps your code simpler and avoids hidden complexity that may not pay off.
8. Limitations and Edge Cases
- Copy-on-write does not mean zero cost. If you mutate large shared data, the copy can still be expensive at that moment.
- It helps most when copies are common and mutations are rare. If every copy is immediately changed, the optimization provides less benefit.
- Threading matters. Copy-on-write makes value behavior safe from accidental shared mutation, but it does not magically make your overall program thread-safe.
- Using isKnownUniquelyReferenced requires reference storage. It is not a general-purpose uniqueness test for every Swift value.
- Some API boundaries can force bridging or additional allocations, especially when working across frameworks or Objective-C-based interfaces.
One common surprise is that benchmarking a single mutation may show a spike in work because that mutation finally pays for the deferred copy.
9. Practical Mini Project
Let us build a tiny clipboard history model that stores text entries and copies efficiently when duplicated. The goal is to keep copying cheap until a history instance is modified.
final class HistoryStorage {
var entries: [String]
init(entries: [String] = []) {
self.entries = entries
}
func copy() -> HistoryStorage {
return HistoryStorage(entries: entries)
}
}
struct ClipboardHistory {
private var storage: HistoryStorage
init() {
self.storage = HistoryStorage()
}
mutating func add(_ text: String) {
if !isKnownUniquelyReferenced(&storage) {
storage = storage.copy()
}
storage.entries.append(text)
}
var entries: [String] {
storage.entries
}
}
var history1 = ClipboardHistory()
history1.add("Hello")
history1.add("World")
var history2 = history1
history2.add("Swift")This mini project demonstrates the classic copy-on-write pattern: copy values cheaply, then duplicate the backing storage only when a mutation requires it.
10. Key Points
- Copy-on-write lets Swift value types share storage until a write occurs.
- It improves performance without changing value semantics.
- Arrays, strings, dictionaries, and sets use it heavily.
- Custom CoW types usually combine a value-type wrapper with reference-type storage.
- isKnownUniquelyReferenced is the key tool for checking whether storage can be mutated in place.
11. Practice Exercise
Practice by predicting when a copy happens in the following scenario, then compare your answer with the solution.
- Create an array named first with five integers.
- Assign it to second.
- Append two values to second.
- Print both arrays.
Expected output: first stays unchanged, while second contains the appended values.
Hint: The copy is deferred until the first mutation of second.
Solution:
var first = [1, 2, 3, 4, 5]
var second = first
second.append(6)
second.append(7)
print(first)
print(second)12. Final Summary
Swift copy-on-write is a performance optimization that keeps value types fast and easy to use. It allows storage to be shared across copies until mutation makes a real duplicate necessary.
That design is a major reason Swift collections feel simple at the language level but still scale well in real applications. You get predictable value semantics, lower memory overhead, and good performance for the common case of copying more often than mutating.
When you design your own types, follow the same idea only when it solves a real problem. For most code, Swift’s standard library already gives you copy-on-write behavior where it matters most. If you want to go further, study value semantics and then profile your code to see where custom storage management is actually worth it.