写时复制(COW)在 Swift 中的应用
原文链接:https://vernsu.github.io/2017/01/20/
标识:Swift学徒
注:本文结尾处有吐槽。
引子
王垠老师在 《Swift 语言的设计错误》一文中指出 array 这种大型数据结构,不应该采用值类型。而应该使用引用类型,将 array 拆分为可变和不可变的两个类。因为值类型的复制代价很大。
王垠老师写到「由于这个原因,没有任何其它现代语言(Java,C#,……)把 array 作为 value type。」看到这句话时,我第一反应是:这是在黑最好的编程语言 PHP不属于「现代语言」(笑),要知道 PHP 中的数组就是 value type。
Swift 中 array 的实现
Swift 语言中的 array 是一个 struct 类型。在具体的实现时,采用了 copy-on-write (写时复制)。
具体来说就是:
array 这个 struct 中存储了指向这个 array 真正 storage 的 reference, 而 array 这个 struct 只是其内部 storage 的一个 owner。
当多个 owner 同时指向这个 storage,其中任意一个 owner 要修改这个 storage 内部的内容时, 就会先复制这个 storage 的内容, 再在复制出来的 storage 内进行真正的修改。
在很多情况下,通过保持容器的引用而不是执行深度拷贝能够节省不必要的内存占用和计算力。如果容器的引用计数大于1并且容器被改变时,就会拷贝底层容器实现。例如下面的情况,当 d 被分配给 c 时不进行拷贝,但当 d 的结构要被改变时,那么 d 就会被拷贝,然后 2 被附加到 d :
var c: [Int] = [ ... ]
var d = c //这里没有拷贝
d.append(2) //这里 *有* 拷贝
进一步优化写时复制
在 Swift 中对于简单的值类型,比如Int、Double、CGPoint等,它们的拷贝时间是个常数。而对于可扩展的数据结构,为了效率考虑,会使用写时复制技术。
Swift官方文档中推荐开发者在实现比较大的数据结构时实现写时复制语义。
之前我们在《值类型内嵌套可变引用类型时值语义的实现》一文中所举的例子已经详细说明了如何自己实现写时复制。如果 backing storeage 非常大,需要实现 COW 语义来优化,那么当然值得更进一步的优化。
Swift 标准库提供可一个函数 isKnownUniquelyReferenced
,用来检查某个实例是不是唯一的引用,如果是,说明该实例没有被共享,我们可以直接修改当前的实例,如果不是,说明实例被共享,这时对它进行更改就需要先复制。
我们利用这个函数优化一下之前的例子:
struct PaintingPlan // 值类型
{
// 私有的引用类型, for "deep storage"
private var bucket = Bucket(color: .blue)
var bucketColor: Color {
get {
return bucket.color
}
set {
//优化
if isKnownUniquelyReferenced(&bucket) {
bucket.color = bucketColor
} else {
bucket = Bucket(color: newValue)
}
}
}
}
Swift 标准库中大量使用了这种技术。
吐槽
吐槽 1
王垠老师在《Java 有值类型吗?》中表示他理解的值类型和你们想的值类型不一样。
意思就是,世界上有两种值类型:
- 王垠的值类型
- 其他人理解的值类型。
吐槽 2
某人:「王垠不管说什么,一定是错的。如果你觉得王垠说的有点道理,那一定是你错了。」
王垠:「你说的很有道理。」
关注公众号(ID:SwiftBetter),进一步探讨主题
Paste_Image.png