什么是Copy-on-Write
寫入時復制(英語:
Copy-on-write,簡稱COW)是一種計算機程序設(shè)計領(lǐng)域的優(yōu)化策略。其核心思想是,如果有多個調(diào)用者(callers)同時請求相同資源(如內(nèi)存或磁盤上的數(shù)據(jù)存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調(diào)用者試圖修改資源的內(nèi)容時,系統(tǒng)才會真正復制一份專用副本(private copy)給該調(diào)用者,而其他調(diào)用者所見到的最初的資源仍然保持不變。這過程對其他的調(diào)用者都是透明的。此作法主要的優(yōu)點是如果調(diào)用者沒有修改該資源,就不會有副本(private copy)被建立,因此多個調(diào)用者只是讀取操作時可以共享同一份資源。
在 Swift 中,Copy-on-Write(寫時復制)是一種優(yōu)化技術(shù),用于在需要進行修改時避免不必要的數(shù)據(jù)復制。它主要用于值類型(value types),如結(jié)構(gòu)體(struct)和枚舉(enum)。
在 Swift 中,當將一個值類型賦值給另一個變量或常量時,通常會發(fā)生值的復制。這意味著原始值的一個副本會被創(chuàng)建,并分配給新的變量或常量。這樣,原始值和副本是完全獨立的,對其中一個進行修改不會影響另一個。
然而,有時候進行這種復制操作是不必要的,特別是當值類型的實例是不可變的(immutable)或者只有一個引用時。為了避免不必要的復制開銷,Swift 使用了 Copy-on-Write 機制。
Copy-on-Write 的基本思想是,當一個不可變的值類型實例被復制時,實際上只會增加一個指向原始數(shù)據(jù)的引用計數(shù)。只有在進行修改操作時,才會對值進行復制,以確保修改操作不會影響到其他引用。
具體來說,當一個不可變的值類型實例被賦值給一個新的變量或常量時,原始值的引用計數(shù)會增加。這樣,原始值和新的變量或常量共享同一個內(nèi)存。當進行第一次修改操作時,Copy-on-Write 機制會檢查原始值的引用計數(shù)。如果引用計數(shù)為 1,表示該值沒有被共享,可以直接進行修改。但如果引用計數(shù)大于 1,表示該值被多個引用共享,此時會進行復制操作,創(chuàng)建一個新的副本,并將修改操作應用在副本上,而不是原始值上。
通過使用 Copy-on-Write 機制,Swift 可以避免不必要的復制開銷,提高性能和內(nèi)存效率。這種優(yōu)化技術(shù)在 Swift 的標準庫中被廣泛應用,特別是在Array、Dictionary、Set這樣的集合類型中。
需要注意的是,Copy-on-Write 僅適用于值類型(value types),對于引用類型(reference types)如類(class),它不會自動應用。對于引用類型,需要手動實現(xiàn)類似的行為,例如使用復制構(gòu)造函數(shù)(copy constructor)或提供自定義的復制方法。
下面,看看 Swift 中 COW 的具體體現(xiàn)。
基本數(shù)據(jù)類型
從下面的打印信息中我們可以看到,對于String、Int等基本類型的數(shù)據(jù)進行賦值時就發(fā)生了拷貝操作。
/// 打印地址
func address(of object: UnsafeRawPointer) {
let addr = Int(bitPattern: object)
print(String(format: "%p", addr))
}
var str1 = "1234"
var str2 = str1
address(of: &str1) //0x100008108
address(of: &str2) //0x100008118
var num1 = 5
var num2 = num1
address(of: &num1) //0x100008128
address(of: &num2) //0x100008130
集合類型
對于集合類型,如下面的arr1和arr2,我們可以看到在對寫入操作前,賦值操作并未發(fā)生拷貝操作;在對arr2進行修改(即寫入)后,arr2的地址發(fā)生變化,也就是說此時發(fā)生了拷貝操作。
var arr1 = [1,2,3,4]
var arr2 = arr1
//修改前,arr1和arr2地址一樣
address(of: &arr1) //0x600001708420
address(of: &arr2) //0x600001708420
//對arr2進行修改
arr2[2] = 0
//修改arr2后,arr2地址變了
address(of: &arr1) //0x600001708420
address(of: &arr2) //0x600001708460
自定義的結(jié)構(gòu)體
我們知道 Swift 中的 COW 適用于值類型數(shù)據(jù),而且通常是集合類型的。那么對于自定義的結(jié)構(gòu)體是否默認也存在這種機制呢?
struct MyStruct {
var data: [Int]
}
var obj1 = MyStruct(data: [1, 2, 3])
var obj2 = obj1
address(of: &obj1) //0x100008148
address(of: &obj2) //0x100008150
obj2.data[0] = 100
address(of: &obj1) //0x100008148
address(of: &obj2) //0x100008150
從上面的打印可以看出,對于自定義的結(jié)構(gòu)體,并不支持COW。
COW的實現(xiàn)
在 Swift 的GitHub官方文檔OptimizationTips.rst中有這樣一段代碼:
/// 將實際值存于class Ref中,以便實現(xiàn)`reference type`
final class Ref<T> { //必須使用final修飾
var val: T
init(_ v: T) { val = v }
}
/// Box包裝`reference type`
struct Box<T> {
var ref: Ref<T>
init(_ x: T) { ref = Ref(x) }
var value: T {
get { return ref.val }
set {
//在進行寫操作前,檢查是否有其他引用,如果有,進行復制
if !isKnownUniquelyReferenced(&ref) {
ref = Ref(newValue)
return
}
ref.val = newValue
}
}
}
struct TestCOW {
var id: Int = 0
}
let test = TestCOW()
var box1 = Box(test)
var box2 = box1
address(of: &box1.ref.val) //0x600000201570
address(of: &box2.ref.val) //0x600000201570
box2.value.id = 1
address(of: &box1.ref.val) //0x600000201570
address(of: &box2.ref.val) //0x600000201d90
需要注意的是class Ref<T>必須使用final修飾,有以下幾個原因:
-
避免類的繼承:將 Ref<T> 類定義為 final 可以防止其他類繼承它。由于 Ref<T> 類是用于支持 Box<T> 結(jié)構(gòu)體的內(nèi)部實現(xiàn),而不是作為可繼承的基類,因此將其定義為
final可以確保它不會被錯誤地繼承和擴展。 -
保持引用計數(shù)的一致性:Ref<T> 類使用 isKnownUniquelyReferenced(_:) 函數(shù)來檢查引用計數(shù),以確定是否需要進行復制。如果 Ref<T> 類是可繼承的,其他子類可能會引入對引用計數(shù)的修改,導致 isKnownUniquelyReferenced(_:) 函數(shù)的結(jié)果不準確。通過將 Ref<T> 類定義為
final,可以確保引用計數(shù)的一致性,從而正確地實現(xiàn) Copy-on-Write 的邏輯。
參考
維基百科中的寫入時復制