Swift 的 Copy-on-Write 是怎么实现的

41 阅读4分钟

Copy-on-Write (写时复制,CoW) 是一种优化策略,主要应用于 Swift 的标准库中的值类型集合(如 Array, Dictionary, Set, String) ,以提高性能。它的核心思想是:

只有当值类型的实例被修改时,才会发生实际的数据复制。在修改之前,多个实例可以安全地共享底层数据存储。

为什么需要 CoW?

对于 Array 这样的集合,如果每次赋值或传递都进行深度复制(将所有元素复制一遍),当集合很大时,这会带来巨大的性能开销。CoW 允许这些集合在不修改的情况下高效地共享底层数据。

CoW 的实现原理:

CoW 通常通过以下机制实现:

  1. 引用计数 (Reference Counting): 尽管 Array 等是值类型,但它们的底层数据存储(例如一个存储元素的缓冲区)通常是一个引用类型。这个引用类型会维护一个引用计数,记录有多少个外部实例正在共享它。

  2. 内部不可变性: 底层数据存储在共享时被认为是逻辑上不可变的。

  3. 检查唯一引用: 当一个值类型的实例(例如 Array)需要被修改时,它会首先检查它当前引用的底层数据存储的引用计数。

    • 如果引用计数为 1 (即该实例是唯一引用者): 不需要复制。实例可以直接修改底层数据。
    • 如果引用计数大于 1 (即有其他实例也共享同一个底层数据): 此时会发生“写时复制”。系统会创建一个底层数据的新副本,并将当前实例的引用更新为指向这个新副本。然后,对新副本进行修改,而原始的底层数据(和它所有的共享者)保持不变。

一个简化的 CoW 示例 (概念模型):

假设我们有一个自定义的 CoW 结构体 MyArray:

// 1. 底层存储是一个引用类型,带有引用计数
private class _ArrayBuffer<Element> {
    var elements: [Element] // 真正的元素存储
    var count: Int { return elements.count }

    init(_ elements: [Element]) {
        self.elements = elements
    }
}

// 2. 结构体包装器,持有对底层缓冲区的引用
struct MyArray<Element> {
    private var buffer: _ArrayBuffer<Element>

    init() {
        buffer = _ArrayBuffer([])
    }

    init(_ elements: [Element]) {
        buffer = _ArrayBuffer(elements)
    }

    // 内部方法,用于确保 buffer 是唯一的,如果不是,则复制
    private mutating func ensureUniqueBuffer() {
        // isKnownUniquelyReferenced(&buffer) 是 Swift 标准库提供的一个优化函数
        // 它会检查一个类的实例是否只有一个强引用。
        // 如果不是唯一引用,或者 buffer 是 nil,就复制
        if !isKnownUniquelyReferenced(&buffer) {
            buffer = _ArrayBuffer(buffer.elements) // 复制操作
            print("Buffer copied!")
        }
    }

    // 可变属性或方法会触发 CoW 逻辑
    var description: String { return "[\(buffer.elements.map { String(describing: $0) }.joined(separator: ", "))]" }

    // 可变操作,会在修改前调用 ensureUniqueBuffer()
    mutating func append(_ newElement: Element) {
        ensureUniqueBuffer() // 检查并复制
        buffer.elements.append(newElement)
    }

    subscript(index: Int) -> Element {
        get {
            return buffer.elements[index]
        }
        set {
            ensureUniqueBuffer() // 检查并复制
            buffer.elements[index] = newValue
        }
    }
}

// 演示
var arr1 = MyArray([1, 2, 3])
var arr2 = arr1 // 此时 arr1 和 arr2 共享同一个 buffer

print("arr1: \(arr1.description)") // arr1: [1, 2, 3]
print("arr2: \(arr2.description)") // arr2: [1, 2, 3]

// 修改 arr2 会触发 CoW
arr2.append(4) // 输出: Buffer copied!
print("arr1: \(arr1.description)") // arr1: [1, 2, 3] (arr1 仍然是旧数据)
print("arr2: \(arr2.description)") // arr2: [1, 2, 3, 4] (arr2 现在指向新数据)

// 修改 arr1 不会触发 CoW,因为此时 arr1 是其 buffer 的唯一引用者
arr1.append(0)
print("arr1: \(arr1.description)") // arr1: [1, 2, 3, 0]

**Swift 标准库中的 CoW:**

SwiftArray, Dictionary, Set, String 等集合类型都内置了 CoW 优化它们的底层实现会维护一个引用计数的缓冲区

例如,当:

var array1 = [1, 2, 3] var array2 = array1 // 此时 array1 和 array2 共享同一个底层数据 ​ array2.append(4)   // 只有在这一步,array2 的底层数据才会被复制,                   // array2 获得一个独立的新副本,array1 保持不变。


**CoW 的优点:**

-   **性能优化:** 避免了不必要的深拷贝,尤其是在处理大型集合时。
-   **内存效率:** 减少了不必要的内存分配,因为数据在未修改时可以共享。
-   **值类型语义:** 尽管底层使用了引用类型,但从外部看,这些集合仍然保持了值类型的行为(每次修改都看起来像是一个新值)。

**CoW 的缺点/注意事项:**

-   **隐含复制:** 开发者需要意识到,在某些看似简单的修改操作(如 append、insert 等)下,可能会发生一次底层数据的复制,这在极端情况下可能会有性能开销。
-   **桥接问题:** 当 Swift 的集合类型与 Objective-C 的对应类型(如 NSArray, NSDictionary)进行桥接时,CoW 行为可能会失效或表现不同,因为 Objective-C 集合是引用类型。

总的来说,Copy-on-Write 是一种强大的底层优化,它让 Swift 的值类型集合在保持值类型语义的同时,也能实现高效的性能。