Swift進階之內存模型和方法調度

bjwelinarkj 7年前發布 | 30K 次閱讀 Swift Apple Swift開發

前言

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>.size
print(size) // 16
let 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>.size
print(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 //12
let 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 = point1
point2.x = 9.0
print(point1.x) //10.0

var person1 = Person(name: "Leo", age: 24) var person2 = person1 person2.age = 25 print(person1.age)//9.0</code></pre>

我們先看看對應內存的使用

值類型有很多優點,其中主要的優點有兩個

  • 線程安全,每次都是獲得一個copy,不存在同時修改一塊內存
  • 不可變狀態,使用值類型,不需要考慮別處的代碼可能會對當前代碼有影響。也就沒有side effect。

ARC是相對于引用類型的。

ARC是一個內存管理機制。當一個引用類型的對象的reference count(引用計數)為0的時候,那么這個對象會被釋放掉。

我們利用XCode 8和iOS開發,來直觀的查看下一個值類型變量的引用計數變化。

新建一個iOS單頁面工程,語言選擇Swift,然后編寫如下代碼

這里寫圖片描述

然后,當斷點停在24行處的時候,Person的引用計數如下

這里,底部的 thread_2673 是主線程堆Person對象的持有,是iOS系統默認添加。所以, var leo = Person(name: "Leo", age: 25) 這一行后,準確的說是引用計數加</f一,并不是引用計數為一。當然,這些系統自動創建的也會自動銷毀,我們無須考慮。

可以看到,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)</code></pre>

可以看到,輸出

16 //point as Point
40 //point as Drawable
32 //line as Line
40 //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),用來存儲協議的函數。

那么,內存結構圖,如下

范型

范型讓代碼支持靜態多態。比如:

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的編譯器優化還會做更多的事情,上述優化雖然代碼變多,但是編譯器還會對代碼進行壓縮。所以,實際上,并不會對二進制包大小有什么影響。

參考資料

 

來自:http://www.jianshu.com/p/6495a6ce65ed

 

 本文由用戶 bjwelinarkj 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!