对于 Swift 中的 Array、Dictionary、Set 和 String 这些实现了 写时复制(Copy-on-Write, CoW) 的集合类型,复制操作的触发必须同时满足两个条件:
1. 触发复制的“两个必要条件”
CoW 不会在赋值时发生,而是在你尝试修改数据时,且满足以下两个条件:
- 存在多个强引用指向同一块底层内存: 当你执行
let arrayB = arrayA时,底层存储的引用计数变为 2。此时,两个变量共享同一块堆内存。 - 执行了写操作(Mutation) : 你调用了会改变集合内容的方法,如
append()、remove()、insert(),或者通过下标(subscript)进行赋值操作。
2. 具体场景分析
A. 赋值与传递(不触发复制)
Swift
let a = [1, 2, 3]
let b = a // 此时 a 和 b 指向同一块内存,引用计数为 2
// 仅仅是拷贝了结构体(包含一个指针),没有内存数据拷贝。
B. 第一次写操作(触发复制)
Swift
var b = a
b.append(4)
// 步骤 1:b 检查底层内存的引用计数,发现是 2。
// 步骤 2:为了不影响 a,b 会先申请一块新内存,将 [1, 2, 3] 拷贝过去。
// 步骤 3:在新的内存上执行 append(4)。
// 结果:a 是 [1, 2, 3],b 是 [1, 2, 3, 4]。
C. 后续写操作(不再触发复制)
Swift
b.append(5)
// 步骤 1:b 检查引用计数,发现已经是 1(独占)。
// 步骤 2:直接在当前内存原地修改(In-place mutation)。
// 性能:这次操作非常快,接近于 C 数组的性能。
3. 常见的触发“误区”
有一些场景可能在你意料之外触发了 CoW 复制:
-
传递给函数并在函数内修改:
Swift
func update(data: [Int]) { var localData = data localData.append(1) // 如果外部还在持有这个 data,这里会触发复制 } -
在结构体方法中修改属性: 如果一个结构体
Container包含一个Array属性,当你拷贝Container实例并修改其中一个实例的数组时,也会触发 CoW。 -
通过下标修改 Dictionary 中的值:
dict["key"] = newValue会触发检查。
4. CoW 的底层判定逻辑(伪代码)
Swift 编译器在处理这些集合的 mutating 方法时,底层逻辑大致如下:
Swift
// 伪代码表示
mutating func append(_ element: Element) {
// 检查引用是否唯一
if !isKnownUniquelyReferenced(&self._storage) {
// 如果不唯一,执行深拷贝
self._storage = self._storage.copy()
}
// 原地修改
self._storage.append(element)
}
5. 如何避免不必要的 CoW 复制?
虽然 CoW 已经很高效,但在高性能场景下,频繁的拷贝依然有开销。你可以通过以下方式优化:
- 使用
unique引用:尽量避免无意义的中间变量赋值。 - 使用
inout参数:将数组传递给函数时,使用inout可以尽可能保持引用唯一,从而实现原地修改。 - 预拨空间:使用
reserveCapacity(_:)。虽然这与 CoW 不是直接一回事,但减少内存重新分配(Reallocation)能进一步提升写操作性能。
总结
CoW 会在 “多个变量共享内存” 且 “其中一个变量试图修改数据” 的那一刻触发。它是 Swift 平衡“值类型安全性”和“引用类型高性能”的核心利器。
英文版
8-27. [Memory Management] When is CoW triggered for struct/array/dictionary?
For Swift's collection types like Array, Dictionary, Set, and String, which implement Copy-on-Write (CoW) , a copy operation is triggered only when two specific conditions are met simultaneously.
1. The "Two Essential Conditions" for CoW
CoW does not happen during assignment. It is triggered only when you attempt to modify the data and satisfy both of the following:
- Multiple strong references point to the same underlying memory: When you execute
let arrayB = arrayA, the reference count of the underlying storage becomes 2. At this point, both variables share the same heap memory. - A Mutation is executed: You call a method that changes the collection's content, such as
append(),remove(),insert(), or perform an assignment via a subscript.
2. Detailed Scenario Analysis
A. Assignment and Passing (No Copy Triggered)
Swift
let a = [1, 2, 3]
let b = a // a and b point to the same memory; reference count is 2.
// Only the struct (containing a pointer) is copied; no memory data is copied.
B. The First Write Operation (Copy Triggered)
Swift
var b = a
b.append(4)
// Step 1: b checks the reference count of the underlying memory and finds it is 2.
// Step 2: To avoid affecting 'a', 'b' allocates new memory and copies [1, 2, 3] over.
// Step 3: The append(4) is executed on the new memory.
// Result: 'a' remains [1, 2, 3], while 'b' becomes [1, 2, 3, 4].
C. Subsequent Write Operations (No Further Copying)
Swift
b.append(5)
// Step 1: b checks the reference count and finds it is now 1 (unique).
// Step 2: In-place mutation is performed directly on the current memory.
// Performance: This operation is extremely fast, similar to a C-style array.
3. Common "Misconceptions" about Triggers
Certain scenarios might trigger a CoW copy unexpectedly:
-
Passing to a function and modifying inside:
Swift
func update(data: [Int]) { var localData = data localData.append(1) // If the caller still holds 'data', this triggers a copy. } -
Modifying a property within a struct method: If a struct
Containerholds anArray, copying theContainerinstance and then modifying the array in one of the instances will trigger CoW. -
Modifying a value in a Dictionary via subscript:
dict["key"] = newValuetriggers the uniqueness check.
4. Underlying CoW Logic (Pseudo-code)
When the Swift compiler handles mutating methods for these collections, the underlying logic looks roughly like this:
Swift
// Pseudo-code representation
mutating func append(_ element: Element) {
// Check if the reference is unique
if !isKnownUniquelyReferenced(&self._storage) {
// If not unique, perform a deep copy
self._storage = self._storage.copy()
}
// Perform in-place mutation
self._storage.append(element)
}
5. How to Avoid Unnecessary CoW Copies?
While CoW is efficient, frequent copying still incurs overhead in high-performance scenarios. You can optimize this by:
- Maintaining Unique References: Avoid unnecessary intermediate variable assignments.
- Using
inoutParameters: Passing an array to a function usinginouthelps keep the reference unique, allowing for in-place mutation. - Reserving Capacity: Use
reserveCapacity(_:). While not the same as CoW, reducing memory reallocation further improves write performance.
Summary
CoW is triggered the moment "multiple variables share memory" and "one variable attempts to modify the data." It is Swift's core mechanism for balancing the safety of value types with the high performance of reference types.