在 Swift 中,写时复制(Copy-on-Write, CoW) 并不是某种由硬件自动完成的魔法,而是一套由 Swift 编译器在 SIL(Swift 中间语言) 层级精心编排的逻辑。
理解 SIL 层的 CoW,能让你看清编译器是如何在“保证值语义”和“追求极致性能”之间做权衡的。
1. SIL 层的 CoW 实现原理
在 SIL 层,CoW 的实现逻辑主要依赖于一个核心指令:is_unique(对应源码中的 isKnownUniquelyReferenced)。
核心执行流:
当你在 Swift 源码中修改一个支持 CoW 的类型(如 Array)时,编译器生成的 SIL 代码通常包含以下逻辑分支:
-
引用计数检查:调用
is_unique指令检查底层存储(Buffer)的强引用计数。 -
条件分支 (
cond_br) :- 如果是唯一引用 (True) :跳转到**原地修改(In-place Mutation)**路径。
- 如果不是唯一引用 (False) :跳转到拷贝路径(Copy Path) 。
-
深拷贝与重新赋值:在拷贝路径中,调用内存拷贝函数分配新内存,并更新结构体内部的指针。
-
执行修改:无论走哪条路径,最终都在“唯一的”内存块上执行写操作。
2. SIL 层的核心指令
在查看 swiftc -emit-sil 的输出时,你会看到以下关键操作:
begin_access [modify]:标记对变量修改访问的开始,用于执行强制的内存安全检查(Exclusive Access)。is_unique %0 : $InternalBuffer:这是 CoW 的核心,检查操作。strong_retain/strong_release:SIL 优化器会紧盯这两个指令。如果它们在修改前不必要地增加了计数,就会误触发拷贝。
3. SIL 提供的优化机会
SIL 之所以是性能优化的关键,是因为它能在生成机器码之前,通过分析数据流来消除冗余的拷贝。
A. 拷贝消除 (Copy Elimination)
如果 SIL 优化器分析出某个变量虽然被赋值给了另一个变量,但在写操作发生之前,旧变量已经不再被使用(Dead),它会尝试移除不必要的 strong_retain。
- 效果:
is_unique检查会因为引用计数及时降回 1 而返回true,从而将本该发生的深拷贝降级为原地修改。
B. 循环不变量外提 (Loop Invariant Hoisting)
在循环中修改数组时,如果编译器能证明数组的引用状态在循环期间不会改变(即没有外部干扰):
- 优化:它会将
is_unique检查提升到循环外部。 - 结果:循环内部只剩下纯粹的内存写入,不再重复执行昂贵的计数检查。
C. 规避“伪共享”产生的拷贝
有时代码中的临时变量(如在 closure 中捕获)会产生额外的引用计数。SIL 的 ARC 优化 pass 会尝试缩短这些引用的生命周期,确保在 mutating 方法调用那一刻,引用计数尽可能处于最小状态。
4. 为什么要在 SIL 层做这些?
如果等到 LLVM IR 阶段再做优化,编译器看到的只是内存指针和位操作,它已经丢失了“这是一个 Swift 数组”或“这是一个 CoW 结构体”的概念。
而在 SIL 阶段,编译器知道:
- 对象的语义(它知道这是
Array的底层 Buffer)。 - 所有权模型(它知道谁拥有这个对象的强引用)。
- 修改意图(它能区分哪些操作是读,哪些是写)。
总结
在 SIL 层,CoW 是通过 “检查 -> 分支 -> 潜在拷贝 -> 修改” 这一套精密的指令组合实现的。SIL 优化器的核心任务就是通过 ARC 分析,尽可能让 is_unique 返回 true,从而将 的拷贝操作转化为 的内存写入。