1. 程式人生 > >Swift Copy-On-Write 寫時複製

Swift Copy-On-Write 寫時複製

什麼是COW

我們都知道Swift有值型別和引用型別,而值型別在被賦值或被傳遞給函式時是會被拷貝的。在Swift中,所有的基本型別,包括整數、浮點數、字串、陣列和字典等都是值型別,並且都以結構體的形式實現。那麼,我們在寫程式碼時,這些值型別每次賦值傳遞都是會重新在記憶體裡拷貝一份嗎?

答案是否定的,想象一下,假如有個包含上千個元素的陣列,然後你把它copy一份給另一個變數,那麼Swift就要拷貝所有的元素,即使這兩個變數的陣列內容完全一樣,這對它效能來說是多麼糟糕。

The description above refers to the “copying” of strings, arrays, and dictionaries. The behavior you see in your code will always be as if a copy took place. However, Swift only performs an actual copy behind the scenes when it is absolutely necessary to do so. Swift manages all value copying to ensure optimal performance, and you should not avoid assignment to try to preempt this optimization.

而這個優化方式就是 Copy-On-Write(寫時複製),即只有當這個值需要改變時才進行復制行為。

例子

首先,讓我們看下面的例子我們更容易理解,我們建立了陣列arr1,然後將arr1賦值給arr2,再給arr2陣列新增多一個元素,我們通過檢視其地址變化來確定是否進行了拷貝行為。

let arr1 = [1, 2, 3, 4]
var arr2 = arr1
//斷點1
arr2.append(2) 
//斷點2

由於網上很多有關獲取記憶體地址的方法打印出來有差異,在此,使用lldb命令fr v -R [object] 來檢視物件記憶體結構。

斷點1位置,列印arr1, arr2

記憶體結構如下,我們可以看到arr1arr2記憶體地址都是0x000060400047e480,說明arr1arr2此時是共享同一個例項

(lldb) fr v -R arr1
(Swift.Array<Swift.Int>) arr1 = {
  _buffer = {
    _storage = {
      rawValue = 0x000060400047e480 {
        Swift._ContiguousArrayStorageBase = {
          Swift._SwiftNativeNSArrayWithContiguousStorage = {
            Swift._SwiftNativeNSArray = {}
} countAndCapacity = { _storage = { count = { _value = 4 } _capacityAndFlags = { _value = 8 } } } } } } } } (lldb) fr v -R arr2 (Swift.Array<Swift.Int>) arr2 = { _buffer = { _storage = { rawValue = 0x000060400047e480 { Swift._ContiguousArrayStorageBase = { Swift._SwiftNativeNSArrayWithContiguousStorage = { Swift._SwiftNativeNSArray = {} } countAndCapacity = { _storage = { count = { _value = 4 } _capacityAndFlags = { _value = 8 } } } } } } } }

斷點2位置,此時arr2添加了新元素,列印arr2,記憶體結構如下,我們可以看到arr2記憶體地址已經變成了0x00006000000b32c0,說明此時它們不再共享同一個例項,arr2對應的值進行了拷貝行為

(lldb) fr v -R arr2 
(Swift.Array<Swift.Int>) arr2 = {
  _buffer = {
    _storage = {
      rawValue = 0x00006000000b32c0 {
        Swift._ContiguousArrayStorageBase = {
          Swift._SwiftNativeNSArrayWithContiguousStorage = {
            Swift._SwiftNativeNSArray = {}
          }
          countAndCapacity = {
            _storage = {
              count = {
                _value = 5
              }
              _capacityAndFlags = {
                _value = 16
              }
            }
          }
        }
      }
    }
  }
}

由此可見,arr2未做修改時,arr1arr2是共享同一個例項

具體實現

在結構體內部儲存了一個指向實際資料的引用reference,在不進行修改操作的普通傳遞過程中,都是將內部的reference的應用計數+1,在進行修改時,對內部的reference做一次copy操作,再在這個複製出來的資料進行真正的修改,防止和之前的reference產生意外的資料共享

值型別內嵌引用型別

我們已經知道值型別在不進行修改操作的普通資料傳遞時不進行拷貝行為,但是修改時就會進行拷貝行為,但是所有的值型別都是這樣的嗎,如果,這個值型別內嵌了引用型別呢?

class TestClass {
    var value: String
    init(value: String) {
        self.value = value
    }
}

struct TestStruct {
    var testClass = TestClass(value: "hello")
}

var test1 = TestStruct()
var test2 = test1

print(test1.testClass.value)
print(test2.testClass.value)
// 斷點1
test1.testClass.value = "hello world"
// 斷點2
print(test1.testClass.value)
print(test2.testClass.value)

其列印結果如下:

hello
hello
hello world
hello world

再用lldb檢視下其記憶體結構:

// 斷點1 位置 test1 和 test2 的記憶體結構 
(lldb) fr v -R test1
(TestTool.TestStruct) test1 = {
  testClass = 0x0000000101839aa0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162cc "hello"
          }
        }
        _countAndFlags = {
          _value = 5
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}
(lldb) fr v -R test2
(TestTool.TestStruct) test2 = {
  testClass = 0x0000000101839aa0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162cc "hello"
          }
        }
        _countAndFlags = {
          _value = 5
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}

test1 賦值給test2 後,它們的記憶體地址都是0x0000000101839aa0,其引用型別例項變數 testClass 的地址也都是 0x00000001005162cc ,它們共享同一個例項,其引用型別的例項變數也共享

// 斷點2 位置 test1 和 test2 的記憶體結構 
(lldb) fr v -R test1
(TestTool.TestStruct) test1 = {
  testClass = 0x0000000101839aa0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162c0 "hello world"
          }
        }
        _countAndFlags = {
          _value = 11
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}
(lldb) fr v -R test2
(TestTool.TestStruct) test2 = {
  testClass = 0x0000000101839aa0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162c0 "hello world"
          }
        }
        _countAndFlags = {
          _value = 11
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}

而執行test1.testClass.value = "hello world" 後,test1test2 的記憶體地址不變,其例項變數 testClass 地址都改變且相同,還是共享同一個例項變數,也就是說,雖然對值型別有所修改,但沒有進行拷貝行為

那麼如果直接修改整個testClass 呢?

test1.testClass = TestClass(value: "12345")

print(test1.testClass.value)
print(test2.testClass.value)

列印結果為:

12345
hello world

此時,再用lldb檢視下其記憶體結構

(lldb) fr v -R test1
(TestTool.TestStruct) test1 = {
  testClass = 0x0000000101a14de0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162cc "12345"
          }
        }
        _countAndFlags = {
          _value = 5
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}

(lldb) fr v -R test2
(TestTool.TestStruct) test2 = {
  testClass = 0x0000000101839aa0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162c0 "hello world"
          }
        }
        _countAndFlags = {
          _value = 11
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}

由此可見,直接修改testClass變數,test1test1.testClass 的記憶體地址都變化,而test2test2.testClass 記憶體地址不變,說明,此時對結構體進行了拷貝行為,而testClass 這個引用型別是直接指向另一個例項,而不是對原例項進行修改

手動 COW

那麼,如何上面的值型別做到寫時複製呢?

我們可以讓testClass 私有化,讓外部無法對這個引用型別進行修改,再提供一個介面控制這個引用型別的寫入操作,如下所示:

struct TestStruct {
    private var testClass = TestClass(value: "hello")

    var testValue: String {
        get {
            return testClass.value
        }
        set {
            testClass = TestClass(value: newValue)
        }
    }
}

那麼對TestStruct這個結構體,可以通過計算型屬性testValue來控制引用型別的修改,進行修改testClass的值時,直接指向一個新的例項,而非修改,保證了其實現寫時複製

進一步優化

在Swift提供一個函式isKnownUniquelyReferenced,能檢查一個類的例項是不是唯一的引用,如果是,說明例項沒有被共享,我們就不需要對結構體例項進行復制,如果不是,說明例項被共享,這時對它進行更改就需要先複製。

TestStruct 優化如下:

struct TestStruct {
    private var testClass = TestClass(value: "hello")

    var testValue: String {
        get {
            return testClass.value
        }
        set {
            if isKnownUniquelyReferenced(&testClass) {
                testClass.value = newValue
            }
            else {
                testClass = TestClass(value: newValue)
            }
        }
    }
}

參考

相關推薦

Swift Copy-On-Write 複製

什麼是COW 我們都知道Swift有值型別和引用型別,而值型別在被賦值或被傳遞給函式時是會被拷貝的。在Swift中,所有的基本型別,包括整數、浮點數、字串、陣列和字典等都是值型別,並且都以結構體的形式實現。那麼,我們在寫程式碼時,這些值型別每次賦值傳遞都是會重

Copy-On-Write複製機制與Java中CopyOnWriteArrayList容器原始碼實現

Copy-on-Write機制簡稱COW,是一種併發設計策略。其基本思路是多執行緒同時共享同一個內容,當某個執行緒想要修改這個內容的時候,才會真正的把內容copy出去形成一個新的內容然後修改,其它的執行緒繼續讀舊的內容,直到修改完成。這是一種延時懶惰策略。 Copy-on-Write有

Copy-On-Write(寫入複製)技術

看google的hdfs論文時看到這個概念。 Copy-On-Write屬於邏輯快照的一種,還有一種物理快照,百度了一下 ,快照裡邊概念還挺多,主要是以前沒接觸過。以後再整理。 Copy-On-Write是寫入時才複製的意思,找到兩個例子 1.往磁碟寫資料,先寫到的是記憶體

Java 中的複製 (Copy on Write, COW)

Background 寫時複製 (Copy on Write, COW) 有時也叫 "隱式共享", 顧名思義, 就是讓所有需要使用資源 R 的使用者共享資源 R 的同一個副本, 當其中的某一個使用者要對資源 R 進行修改操作時, 先複製 R 的一個副本 R' , 再進行修改操作; Problem 在 J

從win32中的複製Copy on write )機制談起

我們知道,記憶體對映檔案的物理儲存器來自磁碟上已有的檔案,而不是來自也交換檔案。系統在載入exe和dll檔案的時候使用的是記憶體對映檔案來載入並執行exe和dll,這樣大大節省了頁交換檔案的空間以及應用程式的啟動時間。所以,實際上系統載入exe檔案的時候就是利用記憶體對映檔

PHP中的複製Copy On Write

問題引入   首先來看看PHP中的賦值與引用問題 <?php $a = 10;//將常量值賦給變數,會為a分配記憶體空間 $b = $a;//變數賦值給變數,是不是copy了一份副本,b也分配了記憶體空間呢? $c = &$a;//引用是不

copy-on-write複製

寫時複製頁面保護機制是一種優化,記憶體管理器利用它可以節約記憶體。     當程序為一個包含讀/寫頁面的記憶體區物件映射了一份寫時檢視,而並非在對映該檢視時建立一份程序私有的拷貝(Hewlett、Packard、OpenVMS作業系統就是這樣做的)時,記憶體管理器將頁面拷

Linux拷貝技術(copy-on-write)

但是 現在 進程地址空間 優化 如何 進程創建 http exe fork COW技術初窺: 在Linux程序中,fork()會產生一個和父進程完全相同的子進程,但子進程在此後多會exec系統調用,出於效率考慮,linux中引入了“寫時復制“技術,也就是只有進程

拷貝COW(copy-on-write

display 語句 namespace div str pre style -a [0 寫時拷貝技術是通過"引用計數"實現的,在分配空間的時候多分配4個字節,用來記錄有多少個指針指向塊空間,當有新的指針指向這塊空間時,引用計數加一,當要釋放這塊空間時,引用計數減一

string類的簡單實現(拷貝Copy-on-write

前言:上一篇文章實現了string的深拷貝寫法;那我們能不能用淺拷貝寫string類呢?當然可以; 一、 (1) 當我們需要對拷貝之後的物件進行修改時,採用深拷貝的方式; 如果不需要修改,只是輸出字串的內容時,或者是當偶爾修改的的時候,我們再採用深拷貝的方

Linux拷貝技術(copy-on-write)及fork、vfork流程介紹

COW技術初窺:       在Linux程式中,fork()會產生一個和父程序完全相同的子程序,但子程序在此後多會exec系統呼叫,出於效率考慮,linux中引入了“寫時複製“技術,也就是隻有程序空間的各段的內容要發生變化時,才會將父程序的內容複製一份給子程序。       那麼子程序的物理空間沒有程式碼

【C++】c++拷貝Copy On Write

Copy On Write Copy On Write(寫時複製)使用了“引用計數”(reference counting),會有一個變數用於儲存引用的數量。當第一個類構造時,string的建構函式會根據傳入的引數從堆上分配記憶體,當有其它類需要

(轉)Linux拷貝技術(copy-on-write)

轉自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html COW技術初窺:       在Linux程式中,fork()會產生一個和父程序完全相同的子程序,但子程序在此後多會exec系統呼叫,出於效率考慮,linux中引入了“

拷貝(copy on write)

寫時拷貝和傳統深拷貝的區別:深拷貝是,每建立一個物件,則開闢一塊空間,不管讀寫 而寫時拷貝是用一塊空間count計數指向同一塊空間指標的數量。 如果只讀不寫,則只需要開闢一次空間。效率很高,記憶體佔用

拷貝技術(copy-on-write)

 傳統的fork()系統呼叫直接把所有的資源複製給新建立的程序。這種實現過於簡單並且效率低下,因為它拷貝的資料也許並不共享,更糟的情況是,如果新程序打算立即執行一個新的映像,那麼所有的拷貝都將前功盡棄。Linux的fork()使用寫時拷貝(copy-on-write)頁實現

拷貝Copy-On-Write技術

1、概念 Scott Meyers在《More Effective C++》 中舉了個例子,不知你是否還記得?在你還在上學的時候,你的父母要你不要看電視,而去複習功課,於是你把自己關在房間裡,做出一副正在複習功課的樣子,其 實你在幹著別的諸如給班上的某位女生寫情書之類的事,而一旦你的父母出來在你房間要檢查你是

拷貝技術:Copy-On-Write

寫時拷貝技術 1、概念 Scott Meyers在《More Effective C++》中舉了個例子,不知你是否還記得?在你還在上學的時候,你的父母要你不要看電視,而去複習功課,於是你把自己關在房間裡,做出一副正在複習功課的 樣子,其實你在幹著別的諸如給班上的某位女生寫情

拷貝(Copy On Write)方案詳解

class String { public: String(char * str = "" ) //不能strlen(NULL) { _str = new char[strlen( str) + 5];

寫實複製原理(copy-on-write)

CopyOnWrite特點 讀寫併發時無需加鎖,一般用於讀多寫少的操作,但要注意的是,讀資料不能保證實時性 以CopyOnWriteArrayList原始碼進行分析 屬性 // 顯示操作的重入鎖物件 final transient ReentrantLock lock = new ReentrantL

【轉】Copy-On-Write技術 [ linux fork程序使用技術]

inux核心在使用fork建立程序時,基本上會使用Copy-On-Write(COW)技術。這裡解釋一下COW技術以及為什麼在fork中使用。 WIKI上對COW的解釋: Copy-on-write (sometimes referred to as "COW") is an optimization