Swift進階之記憶體模型和方法調度

來源:互聯網
上載者:User

標籤:print   design   des   指定   for   ddc   c++   建議   定義   

前言

Apple今年推出了Swift3.0,較2.3來說,3.0是一次重大的升級。關於這次更新,在這裡都可以找到,最主要的還是提高了Swift的效能,最佳化了Swift API的設計(命名)規範。

前段時間對之前寫的一個項目ImageMaskTransition做了簡單遷移,先保證能在3.0下正常運行,只用了不到30分鐘。總的來說,這次遷移還是非常輕鬆的。但是,有一點要注意:3.0的API設計規範較2.3有了質變,建議做遷移的開發人員先看下WWDC的Swift API Design Guidelines。後面有時間了,我有可能也會總結下。

記憶體配置

通過查看Github上Swift的原始碼語言分布

可以看到

  • Swift語言是用C++寫的
  • Swift的核心Library是用Swift自身寫的。

對於C++來說,記憶體區間如下

  • 堆區
  • 棧區
  • 代碼區
  • 全域靜態區

Swift的記憶體區間和C++類似。也有儲存代碼和全域變數的區間,這兩種區間比較簡單,本文更多專註於以下兩個記憶體區間。

  • Stack(棧),儲存實值型別的臨時變數,函數調用棧,參考型別的臨時變數指標
  • Heap(堆),儲存參考型別的執行個體

在棧上分配和釋放記憶體的代價是很小的,因為棧是一個簡單的資料結構。通過移動棧頂的指標,就可以進行記憶體的建立和釋放。但是,棧上建立的記憶體是有限的,並且往往在編譯期就可以確定的。

舉個很簡單的例子:當一個遞迴函式,陷入死迴圈,那麼最後函數調用棧會溢出。

例如,一個沒有參考型別Struct的臨時變數都是在棧上儲存的

struct Point{    var x:Double // 8 Bytes    var y:Double // 8 bytes}let size = MemoryLayout<Point>.sizeprint(size) // 16let point1 = Point(x:5.0,y:5.0)let instanceSize = MemoryLayout<Point>.size(ofValue: point1)print(instanceSize) //16
那麼,這個記憶體結構

Tips: 圖中的每一格都是一個Word大小,在64位處理器上,是8個位元組

在堆上可以動態按需分配記憶體,每次在堆上分配記憶體的時候,需要尋找堆上能提供相應大小的位置,然後返回對應位置,標記指定位置大小記憶體被佔用。

在堆上能夠動態分配所需大小的記憶體,但是由於每次要尋找,並且要考慮到多線程之間的安全執行緒問題,所以效能較棧來說低很多。

比如,我們把上文的struct改成class,

class PointClass{    var x:Double = 0.0    var y:Double = 0.0}let size2 = MemoryLayout<PointClass>.sizeprint(size2) //8 let point2 = Point(x:5.0,y:5.0)let instanceSize = MemoryLayout<Point>.size(ofValue: point2)print(instanceSize) //8
這時候的記憶體結構

Tips: 圖中的每一格都是一個Word大小,在64位處理器上,是8個位元組

Memory Alignment(記憶體對齊)

和C/C++/OC類似,Swift也有Memory Alignment的概念。舉個直觀的例子
我們定義這樣兩個Struct

struct S{    var x:Int64    var y:Int32}struct SReverse{    var y:Int32    var x:Int64}

然後,用MemoryLayout來擷取兩個結構體的大小

let sSize = MemoryLayout<S>.size //12let sReverseSize = MemoryLayout<SReverse>.size //16

可以看到,只不過調整了結構體中的聲明順序,其佔用的記憶體大小就改變了,這就是記憶體對齊。

我們來看看,記憶體對齊後的記憶體空間分布:

記憶體對齊的原因是,

現代CPU每次讀資料的時候,都是讀取一個word(32位處理器上是4個位元組,64位處理器上是8個位元組)。

記憶體對齊的優點很多

  • 保證對一個成員的訪問在一個Transition中,提高了訪問速度,同時還能保證一次操作的原子性。除了這些,記憶體對齊還有很多優點,可以看看這個SO答案
自動引用計數(ARC)

提到ARC,不得不先講講Swift的兩種基本類型:

  • 實值型別,在賦值的時候,會進行值拷貝
  • 參考型別,在賦值的時候,只會進行引用(指標)拷貝

比如,如下代碼

struct Point{ //Swift中,struct是實值型別    var x,y:Double}class Person{//Swift中,class是參考型別    var name:String    var age:Int    init(name:String,age:Int){        self.name = name        self.age = age    }}var point1 = Point(x: 10.0, y: 10.0)var point2 = point1point2.x = 9.0print(point1.x) //10.0var person1 = Person(name: "Leo", age: 24)var person2 = person1person2.age = 25print(person1.age)//9.0
我們先看看對應記憶體的使用實值型別有很多優點,其中主要的優點有兩個 - 安全執行緒,每次都是獲得一個copy,不存在同時修改一塊記憶體 - 不可變狀態,使用實值型別,不需要考慮別處的代碼可能會對當前代碼有影響。也就沒有side effect。ARC是相對於參考型別的。> ARC是一個記憶體管理機制。當一個參考型別的對象的reference count(引用計數)為0的時候,那麼這個對象會被釋放掉。我們利用XCode 8和iOS開發,來直觀的查看下一個實值型別變數的引用計數變化。建立一個iOS單頁面工程,語言選擇Swift,然後編寫如下代碼![這裡寫圖片描述](http://img.blog.csdn.net/20161113111539024)然後,當斷點停在24行處的時候,Person的引用計數如下這裡,底部的`thread_2673`是主線程堆Person對象的持有,是iOS系統預設添加。所以,` var leo = Person(name: “Leo”, age: 25)`這一行後,準確的說是引用計數加一,並不是引用計數為一。當然,這些系統自動建立的也會自動銷毀,我們無須考慮。可以看到,person唯一的引用就是來自`VM:Stack thread`,也就是棧上。因為引用計數的存在,Class在堆上需要額外多分配一個Word來儲存引用計數:

 

當棧上代碼執行完畢,棧會斷掉對Person的引用,引用計數也就減一,系統會斷掉自動建立的引用。這時候,person的引用計數位0,記憶體釋放。

方法調度(method dispatch)

Swift的方法調度分為兩種

  • 靜態調度 static dispatch. 靜態調度在執行的時候,會直接跳到方法的實現,靜態調度可以進行inline和其他編譯期最佳化。
  • 動態調度 dynamic dispatch. 動態調度在執行的時候,會根據運行時(Runtime),採用table的方式,找到方法的執行體,然後執行。動態調度也就沒有辦法像靜態那樣,進行編譯期最佳化。
Struct

對於Struct來說,方法調度是靜態。

struct Point{    var x:Double // 8 Bytes    var y:Double // 8 bytes    func draw(){        print("Draw point at\(x,y)")    }}let point1 = Point(x: 5.0, y: 5.0)point1.draw()print(MemoryLayout<Point>.size) //16

可以看到,由於是Static Dispatch,在編譯期就能夠知道方法的執行體。所以,在Runtime也就不需要額外的空間來儲存方法資訊。編譯後,方法的調用,直接就是變數地址的傳入,存在了代碼區中。

如果開啟了編譯器最佳化,那麼上述代碼被最佳化成Inline後,

let point1 = Point(x: 5.0, y: 5.0)print("Draw point at\(point1.x,point1.y)")print(MemoryLayout<Point>.size) //16
Class

Class是Dynamic Dispatch的,所以在添加方法之後,Class本身在棧上分配的仍然是一個word。堆上,需要額外的一個word來儲存Class的Type資訊,在Class的Type資訊中,儲存著virtual table(V-Table)。根據V-Table就可以找到對應的方法執行體。

class Point{    var x:Double // 8 Bytes    var y:Double // 8 bytes    init(x:Double,y:Double) {        self.x = x        self.y = y    }    func draw(){        print("Draw point at\(x,y)")    }}let point1 = Point(x: 5.0, y: 5.0)point1.draw()print(MemoryLayout<Point>.size) //8

繼承

因為Class的實體會儲存額外的Type資訊,所以繼承理解起來十分容易。子類只需要儲存子類的Type資訊即可。
例如

class Point{    var x:Double // 8 Bytes    var y:Double // 8 bytes    init(x:Double,y:Double) {        self.x = x        self.y = y    }    func draw(){        print("Draw point at\(x,y)")    }}class Point3D:Point{    var z:Double // 8 Bytes    init(x:Double,y:Double,z:Double) {        self.z = z        super.init(x: x, y: y)    }    override func draw(){        print("Draw point at\(x,y,z)")    }}let point1 = Point(x: 5.0, y: 5.0)let point2 = Point3D(x: 1.0, y: 2.0, z: 3.0)let points = [point1,point2]points.forEach { (p) in    p.draw()}//Draw point at(5.0, 5.0)//Draw point at(1.0, 2.0, 3.0)

協議

我們首先看一段代碼

struct Point:Drawable{    var x:Double // 8 Bytes    var y:Double // 8 bytes    func draw(){        print("Draw point at\(x,y)")    }}struct Line:Drawable{    var x1:Double // 8 Bytes    var y1:Double // 8 bytes    var x2:Double // 8 Bytes    var y2:Double // 8 bytes    func draw(){        print("Draw line from \(x1,y1) to \(x2,y2)")    }}let point = Point(x: 1.0, y: 2.0)let memoryAsPoint = MemoryLayout<Point>.size(ofValue: point)let memoryOfDrawable = MemoryLayout<Drawable>.size(ofValue: point)print(memoryAsPoint)print(memoryOfDrawable)let line = Line(x1: 1.0, y1: 1.0, x2: 2.0, y2: 2.0)let memoryAsLine = MemoryLayout<Line>.size(ofValue: line)let memoryOfDrawable2 = MemoryLayout<Drawable>.size(ofValue: line)print(memoryAsLine)print(memoryOfDrawable2)

可以看到,輸出

16 //point as Point40 //point as Drawable32 //line as Line40 //line as Drawable
16和32不難理解,Point含有兩個Double屬性,Line含有四個Double屬性。對應的位元組數也是對的。那麼,兩個40是怎麼回事呢?而且,對於Point來說,40-16=24,多出了24個位元組。而對於Line來說,只多出了40-32=8個位元組。這是因為Swift對於協議類型的採用如下的記憶體模型 - Existential Container。

 

Existential Container包括以下三個部分:

  • 前三個word:Value buffer。用來儲存Inline的值,如果word數大於3,則採用指標的方式,在堆上分配對應需要大小的記憶體
  • 第四個word:Value Witness Table(VWT)。每個類型都對應這樣一個表,用來儲存值的建立,釋放,拷貝等操作函數。
  • 第五個word:Protocol Witness Table(PWT),用來儲存協議的函數。

那麼,記憶體結構圖,如下



[ point ]


[ line ]

範型

範型讓代碼支援靜態多態。比如:

func drawACopy<T : Drawable>(local : T) {  local.draw()}drawACopy(Point(...))drawACopy(Line(...))
那麼,範型在使用的時候,如何調用方法和儲存值呢?

[ 範型 ]

範型並不採用Existential Container,但是原理類似。

  1. VWT和PWT作為隱形參數,傳遞到範型方法裡。
  2. 臨時變數仍然按照ValueBuffer的邏輯儲存 - 分配3個word,如果儲存資料大小超過3個word,則在堆上開闢記憶體儲存。
範型的編譯器最佳化

1. 為每種類合成具體的方法
比如

func drawACopy<T : Drawable>(local : T) {  local.draw()}

在編譯過後,實際會有兩個方法

func drawACopyOfALine(local : Line) {  local.draw()}func drawACopyOfAPoint(local : Point) {  local.draw()}

然後,

drawACopy(local: Point(x: 1.0, y: 1.0))

會被編譯成為

func drawACopyOfAPoint(local : Point(x: 1.0, y: 1.0))

Swift的編譯器最佳化還會做更多的事情,上述最佳化雖然代碼變多,但是編譯器還會對代碼進行壓縮。所以,實際上,並不會對二進位包大小有什麼影響。

參考資料
  • WWDC 2016 - Understanding Swift Performance
  • WWDC 2015 - Optimizing Swift Performance
  • Does Swift guarantee the storage order of fields in classes and structs?

Swift進階之記憶體模型和方法調度

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.