為什么說 Swift 性能相比較于 Objective-C 會更加 好 ?
為什么在編譯 Swift 的時候這么慢 ?
如何更優(yōu)雅的去寫 Swift ?
Swift中的類型
首先,我們先統(tǒng)一一下關(guān)于類型的幾個概念。
- 平凡類型
有些類型只需要按照字節(jié)表示進行操作颠放,而不需要額外工作,我們將這種類型叫做平凡類型 (trivial)吭敢。比如碰凶,Int 和 Float 就是平凡類型,那些只包含平凡值的 struct 或者 enum 也是平凡類型鹿驼。
struct AStruct {
var a: Int
}
struct BStruct {
var a: AStruct
}
// AStruct & BStruct 都是平凡類型
- 引用類型
對于引用類型欲低,值實例是一個對某個對象的引用。復(fù)制這個值實例意味著創(chuàng)建一個新的引用畜晰,這將使引用計數(shù)增加砾莱。銷毀這個值實例意味著銷毀一個引用,這會使引用計數(shù)減少凄鼻。不斷減少引用計數(shù)腊瑟,最后當(dāng)然它會變成 0,并導(dǎo)致對象被銷毀块蚌。但是需要特別注意的是闰非,我們這里談到的復(fù)制和銷毀值,只是對引用計數(shù)的操作峭范,而不是復(fù)制或者銷毀對象本身财松。
struct CStruct {
var a: Int
}
class AClass {
var a: CStruct
}
class BClass {
var a: AClass
}
// AClass & BClass 都是引用類型
- 組合類型
類似 AClass 這類,引用類型包含平凡類型的,其實還是引用類型辆毡,但是對于平凡類型包含引用類型菜秦,我們暫且稱之為組合類型。
struct DStruct {
var a: AClass
}
// DStruct 是組合類型
影響性能的主要因素
主要原因在下面幾個方面:
- 內(nèi)存分配 (Allocation):主要在于 堆內(nèi)存分配 還是 棧內(nèi)存分配舶掖。
- 引用計數(shù) (Reference counting):主要在于如何 權(quán)衡 引用計數(shù)球昨。
- 方法調(diào)度 (Method dispatch):主要在于 靜態(tài)調(diào)度 和 動態(tài)調(diào)度 的問題。
內(nèi)存分配(Allocation)
內(nèi)存分區(qū) 中的 堆 和 棧访锻。
- 堆(heap)**
堆是用于存放進程運行中被動態(tài)分配的內(nèi)存段
,它的大小并不固定,可動態(tài)擴張或 縮減褪尝。當(dāng)進程調(diào)用malloc等函數(shù)分配內(nèi)存時,新分配的內(nèi)存就被動態(tài)添加到堆上(堆被擴張); 當(dāng)利用free等函數(shù)釋放內(nèi)存時,被釋放的內(nèi)存從堆中被剔除(堆被縮減)
- 棧 (stack heap)**
棧又稱堆棧, 是用戶存放程序臨時創(chuàng)建的局部變量
,也就是說我們函數(shù)括弧“{}” 中定義的變量(但不包括static聲明的變量,static意味著在數(shù)據(jù)段
中存放變量)闹获。除此以外, 在函數(shù)被調(diào)用時,其參數(shù)也會被壓入發(fā)起調(diào)用的進程棧中,并且待到調(diào)用結(jié)束后,函數(shù)的返回值 也會被存放回棧中期犬。由于棧的后進先出特點,所以 棧特別方便用來保存/恢復(fù)調(diào)用現(xiàn)場。從這個意義上講,我們可以把堆棻芊蹋看成一個寄存龟虎、交換臨時數(shù)據(jù)的內(nèi)存區(qū)。
在 Swift 中沙庐,對于 平凡類型 來說都是存在 棧 中的鲤妥,而 引用類型 則是存在于 堆 中的,如下圖所示:
我們都知道拱雏,Swift建議我們多用 平凡類型棉安,那么 平凡類型 比 引用類型 好在哪呢?換句話說「在 棧 中的數(shù)據(jù)和 堆 中的數(shù)據(jù)相比有什么優(yōu)勢铸抑?」
-
數(shù)據(jù)結(jié)構(gòu)
- 存放在棧中的數(shù)據(jù)結(jié)構(gòu)較為簡單贡耽,只有一些值相關(guān)的東西
- 存放在堆中的數(shù)據(jù)較為復(fù)雜,如上圖所示鹊汛,會有type报账、retainCount等凿渊。
-
數(shù)據(jù)的分配與讀取
- 存放在棧中的數(shù)據(jù)從棧區(qū)底部推入 (push),從棧區(qū)頂部彈出 (pop),類似一個數(shù)據(jù)結(jié)構(gòu)中的棧识窿。由于我們只能夠修改棧的末端,因此我們可以通過維護一個指向棧末端的指針來實現(xiàn)這種數(shù)據(jù)結(jié)構(gòu)镣典,并且在其中進行內(nèi)存的分配和釋放只需要重新分配該整數(shù)即可所计。所以棧上分配和釋放內(nèi)存的代價是很小。
- 存放在堆中的數(shù)據(jù)并不是直接 push/pop尘颓,類似數(shù)據(jù)結(jié)構(gòu)中的鏈表走触,需要通過一定的算法找出最優(yōu)的未使用的內(nèi)存塊,再存放數(shù)據(jù)泥耀。同時銷毀內(nèi)存時也需要重新插值饺汹。
-
多線程處理
- 棧是線程獨有的,因此不需要考慮線程安全問題痰催。
- 堆中的數(shù)據(jù)是多線程共享的兜辞,所以為了防止線程不安全迎瞧,需同步鎖來解決這個問題題。
綜上幾點逸吵,在內(nèi)存分配的時候凶硅,盡可能選擇 棧 而不是 堆 會讓程序運行起來更加快。
引用計數(shù)(Reference counting)
首先 引用計數(shù) 是一種 內(nèi)存管理技術(shù)扫皱,不需要程序員直接去操作指針來管理內(nèi)存足绅。
而采用 引用計數(shù) 的 內(nèi)存管理技術(shù),會帶來一些性能上的影響韩脑。主要以下兩個方面:
- 需要通過大量的 release/retain 代碼去維護一個對象生命周期氢妈。
- 存放在 堆區(qū) 的是多線程共享的,所以對于 retainCount 的每一次修改都需要通過同步鎖等來保證線程安全段多。
對于 自動引用計數(shù) 來說, 在添加 release/retain 的時候采用的是一個寧可多寫也不漏寫的原則首量,所以 release/retain 有一定的冗余。這個冗余量大概在 10% 的左右(如下圖进苍,圖片來自于iOS可執(zhí)行文件瘦身方法)加缘。
而這也是為什么雖然 ARC 底層對于內(nèi)存管理的算法進行了優(yōu)化,在速度上也并沒有比 MRC 寫出來的快的原因觉啊。這篇文章 詳細(xì)描述了 ARC 和 MRC 在速度上的比較拣宏。
綜上,雖然因為自動引用計數(shù)的引入杠人,大大減少了內(nèi)存管理相關(guān)的事情勋乾,但是對于引用計數(shù)來說,過多或者冗余的引用計數(shù)是會減慢程序的運行的搜吧。
而對于引用計數(shù)來說市俊,還有一個權(quán)衡問題,具體如何權(quán)衡會再后文解釋滤奈。
方法調(diào)度 (Method dispatch)
在 Swift 中, 方法的調(diào)度主要分為兩種:
- 靜態(tài)調(diào)度: 可以進行inline和其他編譯期優(yōu)化摆昧,在執(zhí)行的時候,會直接跳到方法的實現(xiàn)蜒程。
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.編譯后變?yōu)橄旅娴膇nline方式
point.draw()
// 2.運行時绅你,直接跳到實現(xiàn) Point.draw implementation
- 動態(tài)調(diào)度: 在執(zhí)行的時候,會根據(jù)運行時昭躺,采用 V-Table 的方式忌锯,找到方法的執(zhí)行體,然后執(zhí)行领炫。無法進行編譯期優(yōu)化偶垮。V-Table 不同于 OC 的調(diào)度,在 OC 中,是先在運行時的時候先在子類中尋找方法似舵,如果找不到脚猾,再去父類尋找方法。而對于 V-Table 來說砚哗,它的調(diào)度過程如下圖:
因此龙助,在性能上「靜態(tài)調(diào)度 > 動態(tài)調(diào)度」并且「Swift中的V-Table > Objective-C 的動態(tài)調(diào)度」。
協(xié)議類型 (Protocol types)
在 Swift 引入了一個 協(xié)議類型 的概念蛛芥,示例如下:
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 就稱為協(xié)議類型
for d in drawables {
d.draw()
}
在上述代碼中提鸟,Drawable 就稱為協(xié)議類型,由于 平凡類型 沒有繼承仅淑,所以實現(xiàn)多態(tài)上出現(xiàn)了一些棘手的問題称勋,但是 Swift 引入了 協(xié)議類型 很好的解決了 平凡類型 多態(tài)的問題,但是在設(shè)計 協(xié)議類型 的時候有兩個最主要的問題:
- 對于類似 Drawable 的協(xié)議類型來說漓糙,如何去調(diào)度一個方法铣缠?
- 對于不同的類型,具有不同的size昆禽,當(dāng)保存到 drawables 數(shù)組時,如何保證內(nèi)存對齊蝇庭?
對于第一個問題醉鳖,如何去調(diào)度一個方法?因為對于 平凡類型 來說哮内,并沒有什么虛函數(shù)指針盗棵,所以在 Swift 中并沒有 V-Table 的方式,但是還是用到了一個叫做 The Protocol Witness Table (PWT) 的函數(shù)表北发,如下圖所示:
對于每一個 Struct:Protocol 都會生成一個 StructProtocol 的 PWT纹因。
對于第二個問題,如何保證內(nèi)存對齊問題琳拨?
有一個簡單粗暴的方式就是瞭恰,取最大的Size作為數(shù)組的內(nèi)存對齊的標(biāo)準(zhǔn),但是這樣一來不但會造成內(nèi)存浪費的問題狱庇,還會有一個更棘手的問題惊畏,如何去尋找最大的Size。所以為了解決這個問題密任,Swift 引入一個叫做 Existential Container 的數(shù)據(jù)結(jié)構(gòu)颜启。
- Existential Container
這是一個最普通的 Existential Container。
- 前三個word:Value buffer浪讳。用來存儲Inline的值缰盏,如果word數(shù)大于3,則采用指針的方式,在堆上分配對應(yīng)需要大小的內(nèi)存
- 第四個word:Value Witness Table(VWT)口猜。每個類型都對應(yīng)這樣一個表形葬,用來存儲值的創(chuàng)建,釋放暮的,拷貝等操作函數(shù)笙以。(管理 Existential Container 生命周期)
- 第五個word:Protocol Witness Table(PWT),用來存儲協(xié)議的函數(shù)冻辩。
用偽代碼表示如下:
// Swift 偽代碼
struct ExistContDrawable {
var valueBuffer: (Int, Int, Int)
var vwt: ValueWitnessTable
var pwt: DrawableProtocolWitnessTable
}
所以猖腕,對于上文代碼中的 Point 和 Line 最后的數(shù)據(jù)結(jié)構(gòu)大致如下:
這里需要注意的幾個點:
- 在 ABI 穩(wěn)定之前 value buffer 的 size 可能會變,對于是不是 3個 word 還在 Swift 團隊還在權(quán)衡.
- Existential Container 的 size 不是只有 5 個 word恨闪。示例如下:
對于這個大小差異最主要在于這個 PWT 指針倘感,對于 Any 來說,沒有具體的函數(shù)實現(xiàn)咙咽,所以不需要 PWT 這個指針老玛,但是對于 ProtocolOne&ProtocolTwo 的組合協(xié)議,是需要兩個 PWT 指針來表示的钧敞。
OK蜡豹,由于 Existential Container 的引入,我們可以將協(xié)議作為類型來解決 平凡類型 沒有繼承的問題溉苛,所以 Struct:Protocol 和 抽象類就越來越像了镜廉。
回到我們最初的疑問,「在 Swift 中的, Struct:Protocol 比 抽象類 好在哪里愚战?」
- 由于 Swift 只能是單繼承娇唯,所以 抽象類 很容易造成 「上帝類」,而Protocol可以是一個多這多個則沒有這個問題
- 在內(nèi)存分配上上,Struct是在棧中的寂玲,而抽象類是在堆中的塔插,所以簡單數(shù)據(jù)的Struct:Protocol會再性能上比抽象類更加好
- (寫起來更加有逼格算不算?)
但是拓哟,雖然表面上協(xié)議類型確實比抽象類更加的“好”想许,但是我還是想說,不要隨隨便便把協(xié)議當(dāng)做類型來使用彰檬。
為什么這么說伸刃?先來看一段代碼:
struct Pair {
init(_ f: Drawable, _ s: Drawable) {
first = f ; second = s
}
var first: Drawable
var second: Drawable
}
首先,我們把 Drawable 協(xié)議當(dāng)做一個類型逢倍,作為 Pair 的屬性捧颅,由于協(xié)議類型的 value buffer 只有三個 word,所以如果一個 struct(比如上文的Line) 超過三個 word,那么會將值保存到堆中较雕,因此會造成下圖的現(xiàn)象:
一個簡單的復(fù)制碉哑,導(dǎo)致屬性的copy挚币,從而引起 大量的堆內(nèi)存分配。
所以扣典,不要隨隨便便把協(xié)議當(dāng)做類型來使用妆毕。上面的情況發(fā)生于無形之中,你卻沒有發(fā)現(xiàn)贮尖。
當(dāng)然笛粘,如果你非要將協(xié)議當(dāng)做類型也是可以解決的,首先需要把Line改為class而不是struct湿硝,目的就是引入引用計數(shù)薪前。所以,將Line改為class之后关斜,就變成了如下圖所示:
至于修改了 line 的 x1 導(dǎo)致所有 pair 下的 line 的 x1 的值都變了示括,我們可以引入 Copy On Write 來解決。
當(dāng)我們 Line 使用平凡類型時痢畜,由于line占用了4個word垛膝,當(dāng)把協(xié)議作為類型時,無法將line存在 value buffer 中丁稀,導(dǎo)致了堆內(nèi)存分配吼拥,同時每一次復(fù)制都會引發(fā)堆內(nèi)存分配,所以我們采用了引用類型來替代平凡類型二驰,增加了引用計數(shù)而降低了堆內(nèi)存分配扔罪,這就是一個很好的引用計數(shù)權(quán)衡的問題。
泛型(Generic code)
首先桶雀,如果我們把協(xié)議當(dāng)做類型來處理,我們稱之為 「動態(tài)多態(tài)」唬复,代碼如下:
protocol Drawable {
func draw()
}
func drawACopy(local : Drawable) {
local.draw()
}
let line = Line()
drawACopy(line)
// ...
let point = Point()
drawACopy(point)
而如果我們使用泛型來改寫的話矗积,我們稱之為 「靜態(tài)多態(tài)」,代碼如下:
// 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)
而這里所謂的 動態(tài) 和 靜態(tài) 的區(qū)別在哪里呢敞咧?
在 Xcode 8 之前棘捣,唯一的區(qū)別就是由于使用了泛型,所以在調(diào)度方法是休建,我們已經(jīng)可以根據(jù)上下文確定了這個 T 到底是什么類型乍恐,所以并不需要 Existential Container,所以泛型沒有使用 Existential Container测砂,但是因為還是多態(tài)茵烈,所以還是需要VWT和PWT作為隱形參數(shù)傳遞,對于臨時變量仍然按照ValueBuffer的邏輯存儲 - 分配3個word砌些,如果存儲數(shù)據(jù)大小超過3個word呜投,則在堆上開辟內(nèi)存存儲加匈。如圖所示:
這樣的形式其實和把協(xié)議作為類型并沒有什么區(qū)別。唯一的就是沒有 Existential Container 的中間層了仑荐。
但是雕拼,在 Xcode 8 之后,引入了 Whole-Module Optimization 使泛型的寫法更加靜態(tài)化粘招。
首先啥寇,由于可以根據(jù)上下文知道確定的類型,所以編譯器會為每一個類型都生成一個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))
//變?yōu)?drawACopyOfAPoint(local : Point(x: 1.0, y: 1.0))
由于每個類型都生成了一個drawACopy的方法辑甜,drawACopyOfAPoint的調(diào)用就吧編程了一個靜態(tài)調(diào)度,再根據(jù)前文靜態(tài)調(diào)度的時候逊笆,編譯器會做 inline 處理栈戳,所以上面的代碼經(jīng)過編譯器處理之后代碼如下:
drawACopy(local: Point(x: 1.0, y: 1.0))
//會變?yōu)?Point(x: 1.0, y: 1.0).draw()
由于編譯器一步步的處理,再也不需要 vwt难裆、pwt及value buffer了子檀。所以對于泛型來做多態(tài)來說,就叫做靜態(tài)多態(tài)乃戈。
幾點總結(jié)
-
為什么在編譯 Swift 的時候這么慢
- 因為編譯做了很多事情褂痰,例如 靜態(tài)調(diào)度的inline處理,靜態(tài)多態(tài)的分析處理等
-
為什么說 Swift 相比較于 Objective-C 會更加快
- 對于Swift來說症虑,更多的靜態(tài)的缩歪,比如靜態(tài)調(diào)度、靜態(tài)多態(tài)等谍憔。
- 更多的棧內(nèi)存分配
- 更少的引用計數(shù)
-
如何更優(yōu)雅的去寫 Swift
- 不要把協(xié)議當(dāng)做類型來處理
- 如果需要把協(xié)議當(dāng)做類型來處理的時候匪蝙,需要注意 big Value 的復(fù)制就引起堆內(nèi)存分配的問題∠捌叮可以用 Indirect Storage + Copy On Write 來處理逛球。
- 對于一些抽象,可以采用 Struct:Protocol 來代替抽象類苫昌。至少不會有上帝類出現(xiàn)颤绕,而且處理的好的話性能是比抽象類更好的。
參考資料
- Understanding Swift Performance
- 真實世界中的 Swift 性能優(yōu)化
- Exploring Swift Memory Layout
- 水平有限祟身,若有錯誤奥务,希望多多指正!coderonevv#gmail.com
更多
工作之余袜硫,寫了點筆記氯葬,如果需要可以在我的 GitHub 看。