Swift 性能相關(guān)

為什么說 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)颤绕,而且處理的好的話性能是比抽象類更好的。

參考資料

更多

工作之余袜硫,寫了點筆記氯葬,如果需要可以在我的 GitHub 看。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末父款,一起剝皮案震驚了整個濱河市溢谤,隨后出現(xiàn)的幾起案子瞻凤,更是在濱河造成了極大的恐慌,老刑警劉巖世杀,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阀参,死亡現(xiàn)場離奇詭異,居然都是意外死亡瞻坝,警方通過查閱死者的電腦和手機蛛壳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來所刀,“玉大人衙荐,你說我怎么就攤上這事「〈矗” “怎么了忧吟?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長斩披。 經(jīng)常有香客問我溜族,道長,這世上最難降的妖魔是什么垦沉? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任煌抒,我火速辦了婚禮,結(jié)果婚禮上厕倍,老公的妹妹穿的比我還像新娘寡壮。我一直安慰自己,他們只是感情好讹弯,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布况既。 她就那樣靜靜地躺著,像睡著了一般组民。 火紅的嫁衣襯著肌膚如雪坏挠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天邪乍,我揣著相機與錄音,去河邊找鬼对竣。 笑死庇楞,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的否纬。 我是一名探鬼主播吕晌,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼临燃!你這毒婦竟也來了睛驳?” 一聲冷哼從身側(cè)響起烙心,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎乏沸,沒想到半個月后淫茵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡蹬跃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年匙瘪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蝶缀。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡丹喻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出翁都,到底是詐尸還是另有隱情碍论,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布柄慰,位于F島的核電站鳍悠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏先煎。R本人自食惡果不足惜贼涩,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望薯蝎。 院中可真熱鬧遥倦,春花似錦、人聲如沸占锯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽消略。三九已至堡称,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間艺演,已是汗流浹背却紧。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留胎撤,地道東北人晓殊。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像伤提,于是被迫代替她去往敵國和親巫俺。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容