5-4.【性能分析与优化】Copy-on-Write(CoW)在什么场景下反而会成为性能瓶颈?

1 阅读3分钟

一、CoW 的核心假设(先记住)

绝大多数副本是只读的,
写入是少量、可控、集中发生的。

一旦现实不满足这个假设,CoW 就会崩。


二、最常见的 CoW 性能灾难场景

1️⃣ “读-写交错”的大数组流水线

var data = loadHugeArray()

for x in data {
    if shouldModify(x) {
        data.append(transform(x))
    }
}

问题在哪里?

  • data 被遍历(只读视图)

  • 同时又被写

  • 每一次写:

    • iterator 仍然活着
    • Swift 必须复制整个 buffer
  • 多次 CoW = O(n²)

👉 这是 CoW 最经典的反例


2️⃣ 频繁的小写操作作用于“大数据”

var a = bigArray
for i in 0..<1000 {
    a[i] += 1
}

看起来只是 1000 次写?

实际上:

  • a 与原数组共享 buffer
  • 第一次写 → 整块数组复制
  • 拷贝成本 ≈ bigArray.count

👉 写次数少 ≠ 拷贝成本低


3️⃣ CoW + 多层函数传参

func step1(_ a: [Int]) { step2(a) }
func step2(_ a: [Int]) { step3(a) }
func step3(_ a: [Int]) {
    var b = a
    b.append(1)
}

隐藏成本

  • 多层函数都在“共享同一个 buffer”

  • 在最深层写入时:

    • Swift 发现 owner 数量很多
    • 复制成本最高

👉 越晚写,越贵


4️⃣ 多线程 / 并发下的 CoW

let shared = bigArray

DispatchQueue.concurrentPerform(iterations: 4) { _ in
    var local = shared
    local.append(1)
}

发生了什么?

  • 每个线程:

    • 第一次写都要 copy
  • 内存带宽被打满

  • cache line 频繁失效

👉 CoW 在并发下是“复制风暴”


5️⃣ Dictionary / Set 高频更新

var dict = bigDict
for k in keys {
    dict[k] = compute(k)
}

隐患

  • 哈希表本身很大
  • 每次首次写都要整表复制
  • rehash + copy

👉 CoW 对 hash 表比 Array 更伤


6️⃣ 隐式写操作(最阴险)

表面“只读”

let b = a.map { $0 }

实际

  • 新 array
  • 新 buffer
  • 立即分配

或者:

array.sort()   // 写
array.sorted() // 分配 + 拷贝

👉 很多 API 名字不明显


7️⃣ CoW + 桥接(Swift ↔ Objective-C)

let ns = swiftArray as NSArray
swiftArray.append(1)

问题

  • 桥接会固定 buffer
  • Swift 不再能“就地优化”
  • 写入必复制

👉 桥接会“冻结”CoW 优化路径


三、为什么 Objective-C 在这些场景反而更快?

Objective-C 的模型是:

可变就是可变,没有隐式复制

[mutableArray addObject:x];
  • 原地修改
  • 没有“突然整块复制”的惊吓
  • 性能更可预测

👉 大规模、频繁写入 = ObjC 更稳


四、如何避免 CoW 成为瓶颈(实战建议)

✅ 1. 尽早“断开共享”

var a = original
a.reserveCapacity(original.count + extra)
  • 提前触发一次 CoW
  • 后续写都是 cheap

✅ 2. 分离读写集合

let source = array
var target = array
target.removeAll(keepingCapacity: true)

for x in source {
    target.append(process(x))
}

✅ 3. 写入前 ensureUnique(概念)

Swift 标准库内部做了:

isKnownUniquelyReferenced(&buffer)

你在自己封装 buffer 时也应这么做。


✅ 4. 大数据 + 高频写:慎用值语义

在这些场景下:

  • class
  • NSMutableArray
  • UnsafeBufferPointer

反而是正确工具


五、一个判断 CoW 会不会拖慢你的经验法则

如果你的代码满足 任意两条,就要警惕 CoW:

  • 数据量很大
  • 写操作不是一次性的
  • 写发生在深层调用
  • 写发生在并发环境
  • 写操作“看起来很少”

六、一句话结论(记住它)

CoW 擅长“少写多读”,
但讨厌“偷偷写”和“反复写”。