Swift語言性能分析

一、兩個疑惑

  • OC 和 Swift 語言在 Richards 上評測的結(jié)果顯示,Swift 比 OC 快了4倍,Swift同OC相比會更快,具體應歸結(jié)在那些因素上面?
  • 通常一個 Swift 項目少則編譯五六分鐘,多則編譯個半個小時也是不為過的事情,Swift 語言既然比 OC 速度快,但是為何實際開發(fā)中 Swift 編譯卻很慢?

二、全文思路介紹

通常一門語言的好壞,通常取決于下面三個因素:

  • 內(nèi)存分配:主要是指堆內(nèi)存分配和棧內(nèi)存分配。
  • 引用計數(shù):主要至于如何權(quán)衡引用計數(shù)。
  • 方法調(diào)度: 主要在于靜態(tài)調(diào)度和動態(tài)調(diào)度。
    除了上面這三個因素之外,另外還有另個影響因素。首先是編譯器的優(yōu)化;其次是這門語言中的一些其他額外特性,如Swift語言中的對面向協(xié)議的額外處理。

所以在接下來的篇幅中,筆者將重點從編譯器優(yōu)化、內(nèi)存分配優(yōu)化、引用計數(shù)優(yōu)化、方法調(diào)用優(yōu)化以及面向協(xié)議編程的實現(xiàn)細節(jié)這五個方面來談談Swift語言的性能。

三、編譯器優(yōu)化分析

Whole Module Optimizations機制

不得不說編譯內(nèi)部有很多需要開發(fā)者需要掌握的技術(shù)點,筆者打算后期有時間針對編譯相關(guān)的東西做一些整理,順帶介紹iOS中的LLVM編譯器。如上圖所示,這是Swift編譯器中引入的Whole Module Optimizations優(yōu)化機制。在沒有這個機制之前,同絕大多數(shù)的編譯器一樣,編譯器在編譯過程中,會針對每一個源文件先是生成目標文件(.o 文件),然后連接器將不同的目標文件組合起來,最終生成可執(zhí)行程序。

常規(guī)編譯過程

試想整個項目中我們定義了這樣一個函數(shù)

func max<T:Comparable>(x:T, y:T) -> T {
        return y > x ? y : x
   }

但是在實際的整個項目中,只有一處我們按照下面的形式使用到了上面這個max方法。

let x = 1
let y = 2
let r = max(x: x, y: y)

因為有了Whole Module Optimizations機制,編譯器可以清楚的知道整個項目中只是用到了max函數(shù)的Int類型參數(shù)比較。所以在編譯的過程中,編譯器完全可以把max函數(shù)看做是一個只支持Int類型數(shù)值比較的方法,不用再編譯成還需要支持其他類型參數(shù)比較的方法。Swift編譯器類似的優(yōu)化還有很多,Whole Module Optimizations為編譯器提供了更多的信息,使編譯器可以從全局角度出發(fā),做更多的全局優(yōu)化。

優(yōu)化后的編譯過程

四、內(nèi)存分配和引用計數(shù)優(yōu)化分析

4.1堆棧的介紹

一般程序的內(nèi)存區(qū)域,除了代碼段和數(shù)據(jù)段之外,剩下的主要是堆內(nèi)存和棧內(nèi)存。

  • 堆(heap),堆內(nèi)存一般由程序員自己申請、指明大小、釋放,是用于存放進程運行中被動態(tài)分配的內(nèi)存段,它的大小并不固定,可動態(tài)擴張或 縮減。當進程調(diào)用malloc等函數(shù)分配內(nèi)存時,新分配的內(nèi)存就被動態(tài)添加到堆上(堆被擴張); 當利用free等函數(shù)釋放內(nèi)存時,被釋放的內(nèi)存從堆中被剔除(堆被縮減)。
  • 棧 (stack heap)又稱堆棧, 由編譯器自動創(chuàng)建/分配/釋放,是用戶存放程序臨時創(chuàng)建的局部變量,也就是說我們函數(shù)括弧“{}” 中定義的變量(但不包括static聲明的變量,static意味著在數(shù)據(jù)段中存放變量)。除此以外, 在函數(shù)被調(diào)用時,其參數(shù)也會被壓入發(fā)起調(diào)用的進程棧中,并且待到調(diào)用結(jié)束后,函數(shù)的返回值 也會被存放回棧中。由于棧的后進先出特點,所以 棧特別方便用來保存/恢復調(diào)用現(xiàn)場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時數(shù)據(jù)的內(nèi)存區(qū)。

4.2堆棧的深度問題(額外擴充)

既然說到這里,就順帶補個知識點-----棧的深度。筆者喜歡以點帶面,由一點知識點擴充到方方面面,這是一種思考方式,也是一種學習方式。當然也不是無止境的在文章中以點帶面,如果真是這樣,那么估計一篇文章就根本不是給人讀的了,隨便拿出一個“術(shù)語”一篇文章都不一定能說的完。

關(guān)于棧深度問題通常會出現(xiàn)在遞歸中。因為程序在遞歸時,每一層遞歸的臨時變量和參數(shù),都是被保存在棧中的,所以遞歸調(diào)用的深度過多,就會造成??臻g存儲不足。一般來說棧是向下生長的,堆是向上生長的。把內(nèi)存地址像門牌號編號成 1 ~ 10000,棧的使用就是先用第 10000 號內(nèi)存塊,再用第 9999 號內(nèi)存塊,依次減小編號。而堆的話,是先用第 1 號內(nèi)存塊,再用第 2 號內(nèi)存塊,依次增加編號。

堆內(nèi)存可以認為是沒有上限的(除非你的硬盤空間不足),如果消耗光了計算機的內(nèi)存,操作系統(tǒng)還會用硬盤的虛擬內(nèi)存為你提供更多的內(nèi)存,虛擬內(nèi)存和內(nèi)存的讀寫速度幾倍一致。但是如果大量程序占用了虛擬內(nèi)存,很可能會出現(xiàn)內(nèi)存泄露問題。這種情況,虛擬內(nèi)存很快就會被消耗完畢。

棧內(nèi)存不同于堆內(nèi)存,通常編譯器都會指定程序的棧內(nèi)存空間使用的大小,如果棧內(nèi)存使用超出了限制,就會觸發(fā)程序異常退出,即棧溢出錯誤(Stack Over flow)。但是iOS實際開發(fā)中很少出現(xiàn)棧溢出問題,這就從側(cè)面反映出使用的遞歸比較少。蘋果官方文檔指明:對于主線程,棧內(nèi)存為 1 MB;非主線程,棧內(nèi)存為 512 KB。如果想測試這一點,在主線程創(chuàng)建一個大小為100萬的數(shù)組,這是Xcode就會報錯。題外話就到此結(jié)束。

4.3 Swift基于堆棧的優(yōu)化

Swift中,值類型都是存在棧中的,引用類型都是存在堆中的。蘋果官網(wǎng)上明確指出建議開發(fā)者多使用值類型。這里的值類型就是緊密的和棧是綁定在一起的。下面來看看值類型比引用類型好在那里,為何蘋果會如此建議?

  • 數(shù)據(jù)結(jié)構(gòu)

1、存放在棧中的數(shù)據(jù)結(jié)構(gòu)較為簡單,只有一些值相關(guān)的東西。
2、存放在堆中的數(shù)據(jù)較為復雜,會包含type、retainCount等。

  • 數(shù)據(jù)的分配與讀取

1、存放在棧中的數(shù)據(jù)從棧區(qū)底部推入 (push),從棧區(qū)頂部彈出 (pop),類似一個數(shù)據(jù)結(jié)構(gòu)中的棧。由于我們只能夠修改棧的末端,因此我們可以通過維護一個指向棧末端的指針來實現(xiàn)這種數(shù)據(jù)結(jié)構(gòu),并且在其中進行內(nèi)存的分配和釋放只需要重新分配該整數(shù)即可。所以棧上分配和釋放內(nèi)存的代價是很小。
2、存放在堆中的數(shù)據(jù)并不是直接 push/pop,類似數(shù)據(jù)結(jié)構(gòu)中的鏈表,需要通過一定的算法找出最優(yōu)的未使用的內(nèi)存塊,再存放數(shù)據(jù)。同時銷毀內(nèi)存時也需要重新插值。

  • 多線程處理

1、棧是線程獨有的,因此不需要考慮線程安全問題。
2、堆中的數(shù)據(jù)是多線程共享的,所以為了防止線程不安全,需同步鎖來解決這個問題題。

所以基于在內(nèi)存分配方面的考慮,更多的使用棧而不是堆,可以達到優(yōu)化的效果。

4.4 一個實例

為了更好的理解值類型和引用類型的區(qū)別,我們來深入分析一個簡單的例子。

var persons:[Person] = ...
for p in persons {
      //increase RC
      //decrease RC
}

如果這個例子中的 Person 是 class 類型,在遍歷這個數(shù)組的時候,編譯器內(nèi)部會對于每一個遍歷的元素都會執(zhí)行增加和減少引用計數(shù)操作,實際上這是非常消耗性能的。

但是如果通過 Struct 來解決問題,就是另外一種情況了。如果把Person類改成 Struct ,所有的引用計數(shù)將會從編譯器中消失。

但是使用Struct需要注意一點事項,因為在Struct中包含有大齡引用類型成員時,在復制變量時,也會造成大量的引用計數(shù)操作。

struct Person {
   var websit = NSURL("website")
   var name = NSString(string: "name")
   var addr = NSString(string: "address")
}
var person1 = Person()
var person2 = person1

在調(diào)用var person1 = Person()這句代碼的時候,內(nèi)存分配是這樣的:


在調(diào)用var person2 = person1的時候,內(nèi)存分配是這樣的:


這種情況明顯是不能被接受的,但是我們可以通過把引用類型在封裝一層來解決這個問題,代碼如下:

struct Person {
    var person:PersonWrapper = PersonWrapper()
}

class PersonWrapper {
   var websit = NSURL("website")
   var name = NSString(string: "name")
   var addr = NSString(string: "address")
}
var person1 = Person()
var person2 = person1

經(jīng)過這種更改,當發(fā)生對象復制的時候,內(nèi)存中只有PersonWrapper的引用計數(shù)發(fā)生變化,而內(nèi)部的NSURL和兩個NSString的引用計數(shù)不會發(fā)生變化。

五、方法調(diào)用優(yōu)化分析

稍微有點iOS開發(fā)經(jīng)驗的開發(fā)者應該都知道Objective-C 中方法的調(diào)用,從本質(zhì)上來說都是向相應的對象發(fā)送消息。方法經(jīng)編譯器編譯過后一般就變成了objc_msgSend函數(shù),該函數(shù)的第一個參數(shù)是接受消息的對象,第二個參數(shù)是消息的名字,后面的都是消息攜帶的名字,參數(shù)從0到 n 個不等。

正是基于這一點Objective-C 中,我們可以字符串去調(diào)用方法,就可以用變量來傳遞這個字符串,進而可以實現(xiàn)一些運行時動態(tài)調(diào)用,語言提供的 NSSelectorFromString 是一個很好的說明,runtime 也因此被開發(fā)者奉為神器,被廣大開發(fā)這熟知的JSPatch 也是基于這點實現(xiàn)的。因為這種動態(tài)性的設計使得Objective-C 語言變得異常靈活。

但是,凡事都是要付出代價的,Objective-C語言動態(tài)化這種靈活性是以查表的方式找出函數(shù)地址,既然查表操作,當然要付出時間代價。蘋果官網(wǎng)文檔中介紹了方法調(diào)用時,函數(shù)地址查詢過程,蘋果也發(fā)現(xiàn)了這種方式調(diào)用起來會很慢,所以一種這種的辦法就是緩存方法調(diào)用的查詢結(jié)果,但即便是這樣,性能上同將函數(shù)地址硬編碼到代碼中這種方式相比還是有一些差距。

相比于Objective-C,Swift語言直接放棄了Objective-C這個動態(tài)化機制。就這一方面而言,Swift如今算是和很多主流語言保持了一直。因為舍棄了動態(tài)特性,Swift語言勢必比Objective-C快了一些,但在一定程度上丟失了靈活性。相信不久的將來,Swift勢必會引入一些動態(tài)特性,不過目前而言這并不是它的首要目標。

六、面向協(xié)議編程分析

6.1 問題

Swift 鼓勵我們使用值類型,也鼓勵使用協(xié)議,所以Swift中引入了協(xié)議類型的概念,下面代碼中的 Drawable 就是協(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() { ... }
}

// Drawable 稱為協(xié)議類型
let a: Drawable = Point()
let b: Drawable = Line()
let drawables : [Drawable] = [a, b]
for d in drawables {
    d.draw()
}

以上代碼中定義了一個 Drawable 協(xié)議類型,然后值類型 Point 和 Line都實現(xiàn)了這個協(xié)議。代碼的最后將 Point 和 Line 的實例都放到了 [Drawable] 數(shù)組中。

但是會發(fā)現(xiàn) Point 和 Line 實際 Size 大小不同,這樣一個數(shù)組中就存在大小不同的元素了,通常對于一般的數(shù)組而言這是一種災難。因為數(shù)組元素大小不一致,就無法很方便的定位其中的元素。假如我們的數(shù)組真的是把不同大小的元素放到一個數(shù)組里面,那就意味著,如果我們想定位到第 i 個元素,我們需要把第 0 ~ i-1 個元素的大小都算出來,這樣還可以算出第 i 個元素的內(nèi)存偏移量。還有一個簡單粗暴的方式,取最大的 Size 作為數(shù)組的內(nèi)存對齊的標準,但是這樣一來不但會造成內(nèi)存浪費的問題,還會有一個更棘手的問題,如何去尋找最大的Size。

6.2 蘋果解決問題的方式

為了解決上述問題,Swift 引入一個叫做 Existential Container 的數(shù)據(jù)結(jié)構(gòu)。思路是:使用一個額外的容器(Container)來放每個帶有協(xié)議的值類型,而數(shù)組里面放的是一個固定大小的容器。具體的細節(jié)請往下看。


這是一個最普通的 Existential Container,大小一共是5個 word 。

  • 前三個 word 是 Value buffer,用于存放元素的值,如果word數(shù)大于3,則采用指針的方式,在堆上分配對應需要大小的內(nèi)存

  • 第四個word:Value Witness Table(VWT)。每個類型都對應這樣一個表,用來存儲值的創(chuàng)建,釋放,拷貝等操作函數(shù)。(管理 Existential Container 生命周期)

  • 第五個word:Protocol Witness Table(PWT),用于存放協(xié)議(Protocol)對應的函數(shù)的實現(xiàn)函數(shù)地址。

如果待存放的實例對象大于 3 個 world,Swift就會在堆內(nèi)存中申請一塊空間,將該值保存在堆內(nèi)存中,堆內(nèi)存的對應的地址就會保存在 Value Buffer 的第 1 個 word 中。就像下圖這樣。

最終,這種設計使得:

  • 數(shù)組中每個元素的大小都是固定的 5 個 word,解決了數(shù)組元素下標快速定位的問題。
  • 因為有 Value Buffer 的存在,我們可以將不同大小的值類型存放到 Value Buffer 中,小于等于 3 個 word 的值直接存儲,更大的則通過保存引用地址的方式存儲。
  • 通過 Value Witness Table,我們可以找到這個值類型的相關(guān)生命周期的管理函數(shù)。
  • 通過 Protocol Witness Table,我們可以找到協(xié)議的具體實現(xiàn)函數(shù)的地址。
6.3 需要注意的地方

雖然表面上協(xié)議類型確實比抽象類更加的好,蘋果也是大力推薦使用協(xié)議類型。但是并不意味著可以隨隨便便把協(xié)議當做類型來使用。

struct Pair {
    init(f: Drawable, s: Drawable) {
        first = f ; second = s
    }
    var first: Drawable
    var second: Drawable
}

我們把 Drawable 協(xié)議類型作為 Pair 的屬性,因為協(xié)議類型的 value buffer 只有三個 word,如果一個 結(jié)構(gòu)體struct(比如上文的Line) 超過三個 word,將會形成如下結(jié)構(gòu)。


按照上圖所示,如果再執(zhí)行一個賦值操作,就會導致屬性的copy,從而引起大量的堆內(nèi)存分配。這就是濫用協(xié)議類型導致的后果。

當然這個問題是可以通過合理的設計去避免的。需要將Line改為class即可解決問題,而不是再像之前那樣使用 struct,所以說 值類型也不是可以隨便濫用的。 更改后的結(jié)果是:


這里通過引用類型來替代值類型,增加了引用計數(shù)而降低了堆內(nèi)存分配,這就是一個很好的權(quán)衡引用計數(shù)和內(nèi)存分配的問題。

七、總結(jié)

  • 為什么Swift編譯很慢?

因為Swift在編譯的時候做了很多事情,所以消耗時間比較多是正常的。如對類型的分析等。

  • 為什么Swift相比較OC會更快?

編譯器 Whole Module Optimizations 機制的全局優(yōu)化、更多的棧內(nèi)存分配、更少的引用計數(shù)、更多的靜態(tài)、協(xié)議類型的使用等都是Swift比OC更快的原因。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容