起初的疑問源自於「在 Swift 中的, Struct:Protocol 比 抽象類 好在哪裏?」。可是找來找去都是 Swift 性能相關的東西。整理了點筆記,供你們能夠參考一下。git
在正題開始以前,不知道你是否有以下的疑問:程序員
若是你也有相似疑問,但願這篇筆記能幫你解釋一下上面幾個問題的一些緣由。(ps.上面幾個問題都很大,若是有不一樣的想法和了解,也但願你能分享出來,你們一塊兒討論一下。)github
首先,咱們先統一一下關於類型的幾個概念。算法
有些類型只須要按照字節表示進行操做,而不須要額外工做,咱們將這種類型叫作平凡類型 (trivial)。好比,Int 和 Float 就是平凡類型,那些只包含平凡值的 struct 或者 enum 也是平凡類型。編程
struct AStruct {
var a: Int
}
struct BStruct {
var a: AStruct
}
// AStruct & BStruct 都是平凡類型
複製代碼
對於引用類型,值實例是一個對某個對象的引用。複製這個值實例意味着建立一個新的引用,這將使引用計數增長。銷燬這個值實例意味着銷燬一個引用,這會使引用計數減小。不斷減小引用計數,最後固然它會變成 0,並致使對象被銷燬。可是須要特別注意的是,咱們這裏談到的複製和銷燬值,只是對引用計數的操做,而不是複製或者銷燬對象自己。swift
struct CStruct {
var a: Int
}
class AClass {
var a: CStruct
}
class BClass {
var a: AClass
}
// AClass & BClass 都是引用類型
複製代碼
相似 AClass 這類,引用類型包含平凡類型的,其實仍是引用類型,可是對於平凡類型包含引用類型,咱們暫且稱之爲組合類型。數組
struct DStruct {
var a: AClass
}
// DStruct 是組合類型
複製代碼
主要緣由在下面幾個方面:安全
今天主要談一談 內存分區 中的 堆 和 棧。性能優化
堆是用於存放進程運行中被動態分配的內存段
,它的大小並不固定,可動態擴張或 縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張); 當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)數據結構
棧又稱堆棧, 是用戶存放程序臨時建立的局部變量
,也就是說咱們函數括弧「{}」 中定義的變量(但不包括static聲明的變量,static意味着在數據段
中存放變量)。除此之外, 在函數被調用時,其參數也會被壓入發起調用的進程棧中,而且待到調用結束後,函數的返回值 也會被存放回棧中。因爲棧的後進先出特色,因此 棧特別方便用來保存/恢復調用現場。從這個意義上講,咱們能夠把堆棧當作一個寄存、交換臨時數據的內存區。
在 Swift 中,對於 平凡類型 來講都是存在 棧 中的,而 引用類型 則是存在於 堆 中的,以下圖所示:
咱們都知道,Swift建議咱們多用 平凡類型,那麼 平凡類型 比 引用類型 好在哪呢?換句話說「在 棧 中的數據和 堆 中的數據相比有什麼優點?」
綜上幾點,在內存分配的時候,儘量選擇 棧 而不是 堆 會讓程序運行起來更加快。
首先 引用計數 是一種 內存管理技術,不須要程序員直接去操做指針來管理內存。
而採用 引用計數 的 內存管理技術,會帶來一些性能上的影響。主要如下兩個方面:
對於 自動引用計數 來講, 在添加 release/retain 的時候採用的是一個寧肯多寫也不漏寫的原則,因此 release/retain 有必定的冗餘。這個冗餘量大概在 10% 的左右(以下圖,圖片來自於iOS可執行文件瘦身方法)。
而這也是爲何雖然 ARC 底層對於內存管理的算法進行了優化,在速度上也並無比 MRC 寫出來的快的緣由。這篇文章 詳細描述了 ARC 和 MRC 在速度上的比較。
綜上,雖然由於自動引用計數的引入,大大減小了內存管理相關的事情,可是對於引用計數來講,過多或者冗餘的引用計數是會減慢程序的運行的。
而對於引用計數來講,還有一個權衡問題,具體如何權衡會再後文解釋。
在 Swift 中, 方法的調度主要分爲兩種:
struct Point {
var x, y: Double
func draw() {
// Point.draw implementation
}
}
func drawAPoint(_ param: Point) {
param.draw()
}
let point = Point(x: 0, y: 0)
drawAPoint(point)
// 1.編譯後變爲下面的inline方式
point.draw()
// 2.運行時,直接跳到實現 Point.draw implementation
複製代碼
所以,在性能上「靜態調度 > 動態調度」而且「Swift中的V-Table > Objective-C 的動態調度」。
在 Swift 引入了一個 協議類型 的概念,示例以下:
protocol Drawable {
func draw()
}
struct Point : Drawable {
var x, y: Double
func draw() { ... }
}
struct Line : Drawable {
var x1, y1, x2, y2: Double
func draw() { ... }
}
var drawables: [Drawable]
// Drawable 就稱爲協議類型
for d in drawables {
d.draw()
}
複製代碼
在上述代碼中,Drawable 就稱爲協議類型,因爲 平凡類型 沒有繼承,因此實現多態上出現了一些棘手的問題,可是 Swift 引入了 協議類型 很好的解決了 平凡類型 多態的問題,可是在設計 協議類型 的時候有兩個最主要的問題:
對於第一個問題,如何去調度一個方法?由於對於 平凡類型 來講,並無什麼虛函數指針,因此在 Swift 中並無 V-Table 的方式,可是仍是用到了一個叫作 The Protocol Witness Table (PWT) 的函數表,以下圖所示:
對於每個 Struct:Protocol 都會生成一個 StructProtocol 的 PWT。
對於第二個問題,如何保證內存對齊問題?
有一個簡單粗暴的方式就是,取最大的Size做爲數組的內存對齊的標準,可是這樣一來不但會形成內存浪費的問題,還會有一個更棘手的問題,如何去尋找最大的Size。因此爲了解決這個問題,Swift 引入一個叫作 Existential Container 的數據結構。
這是一個最普通的 Existential Container。
用僞代碼表示以下:
// Swift 僞代碼
struct ExistContDrawable {
var valueBuffer: (Int, Int, Int)
var vwt: ValueWitnessTable
var pwt: DrawableProtocolWitnessTable
}
複製代碼
因此,對於上文代碼中的 Point 和 Line 最後的數據結構大體以下:
這裏須要注意的幾個點:
對於這個大小差別最主要在於這個 PWT 指針,對於 Any 來講,沒有具體的函數實現,因此不須要 PWT 這個指針,可是對於 ProtocolOne&ProtocolTwo 的組合協議,是須要兩個 PWT 指針來表示的。
OK,因爲 Existential Container 的引入,咱們能夠將協議做爲類型來解決 平凡類型 沒有繼承的問題,因此 Struct:Protocol 和 抽象類就愈來愈像了。
回到咱們最初的疑問,「在 Swift 中的, Struct:Protocol 比 抽象類 好在哪裏?」
可是,雖然表面上協議類型確實比抽象類更加的**「好」**,可是我仍是想說,不要隨隨便便把協議當作類型來使用。
爲何這麼說?先來看一段代碼:
struct Pair {
init(_ f: Drawable, _ s: Drawable) {
first = f ; second = s
}
var first: Drawable
var second: Drawable
}
複製代碼
首先,咱們把 Drawable 協議當作一個類型,做爲 Pair 的屬性,因爲協議類型的 value buffer 只有三個 word,因此若是一個 struct(好比上文的Line) 超過三個 word,那麼會將值保存到堆中,所以會形成下圖的現象:
一個簡單的複製,致使屬性的copy,從而引發 大量的堆內存分配。
因此,不要隨隨便便把協議當作類型來使用。上面的狀況發生於無形之中,你卻沒有發現。
固然,若是你非要將協議當作類型也是能夠解決的,首先須要把Line改成class而不是struct,目的就是引入引用計數。因此,將Line改成class以後,就變成了以下圖所示:
至於修改了 line 的 x1 致使全部 pair 下的 line 的 x1 的值都變了,咱們能夠引入 Copy On Write 來解決。
當咱們 Line 使用平凡類型時,因爲line佔用了4個word,當把協議做爲類型時,沒法將line存在 value buffer 中,致使了堆內存分配,同時每一次複製都會引起堆內存分配,因此咱們採用了引用類型來替代平凡類型,增長了引用計數而下降了堆內存分配,這就是一個很好的引用計數權衡的問題。
首先,若是咱們把協議當作類型來處理,咱們稱之爲 「動態多態」,代碼以下:
protocol Drawable {
func draw()
}
func drawACopy(local : Drawable) {
local.draw()
}
let line = Line()
drawACopy(line)
// ...
let point = Point()
drawACopy(point)
複製代碼
而若是咱們使用泛型來改寫的話,咱們稱之爲 「靜態多態」,代碼以下:
// Drawing a copy using a generic method
protocol Drawable {
func draw()
}
func drawACopy<T: Drawable>(local : T) {
local.draw()
}
let line = Line()
drawACopy(line)
// ...
let point = Point()
drawACopy(point)
複製代碼
而這裏所謂的 動態 和 靜態 的區別在哪裏呢?
在 Xcode 8 以前,惟一的區別就是因爲使用了泛型,因此在調度方法是,咱們已經能夠根據上下文肯定了這個 T 究竟是什麼類型,因此並不須要 Existential Container,因此泛型沒有使用 Existential Container,可是由於仍是多態,因此仍是須要VWT和PWT做爲隱形參數傳遞,對於臨時變量仍然按照ValueBuffer的邏輯存儲 - 分配3個word,若是存儲數據大小超過3個word,則在堆上開闢內存存儲。如圖所示:
這樣的形式其實和把協議做爲類型並無什麼區別。惟一的就是沒有 Existential Container 的中間層了。
可是,在 Xcode 8 以後,引入了 Whole-Module Optimization 使泛型的寫法更加靜態化。
首先,因爲能夠根據上下文知道肯定的類型,因此編譯器會爲每個類型都生成一個drawACopy的方法,示例以下:
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))
//變爲
drawACopyOfAPoint(local : Point(x: 1.0, y: 1.0))
複製代碼
因爲每一個類型都生成了一個drawACopy的方法,drawACopyOfAPoint的調用就吧編程了一個靜態調度,再根據前文靜態調度的時候,編譯器會作 inline 處理,因此上面的代碼通過編譯器處理以後代碼以下:
drawACopy(local: Point(x: 1.0, y: 1.0))
//會變爲
Point(x: 1.0, y: 1.0).draw()
複製代碼
因爲編譯器一步步的處理,不再須要 vwt、pwt及value buffer了。因此對於泛型來作多態來講,就叫作靜態多態。
工做之餘,寫了點筆記,若是須要能夠在個人 GitHub 看。