最近在使用 Swift 開發項目時,發現編譯時間實在是慢的出奇。每次 git 切換分支以後,都得編譯很久,並且動輒卡死。有時候改了一點小地方想 debug 看下效果,也得編譯那麼好一下子,實在是苦不堪言。因此下決心要好好研究一下,看看有沒有什麼優化 Xcode 編譯時間的好辦法。git
本文中有很多實驗數據,都是對基於現有項目進行的簡單測試,優化效果僅供參考😅。github
第一步就是搞定編譯時間的測算,方法以下。完成了以後就可進入正題了。swift
defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
複製代碼
module 是 Swift 文件的集合,每一個 module 編譯成一個 framework 或可執行程序。在編譯時,Swift 編譯器分別編譯 module 中的每個文件,編譯完成後再連接到一塊兒,最終再輸出 framework 或可執行程序。後端
因爲這種編譯方式侷限於單個文件,因此像有須要跨函數的優化等就可能會受到影響,好比函數內聯、基本塊合併等。所以,編譯時間會變長。xcode
而若是使用全模塊優化,編譯器會先將全部文件合稱爲同一個文件,而後再進行編譯,這樣可以極大的加快編譯速度。好比編譯器瞭解模塊中全部函數的實現,因此它可以確保執行跨函數的優化(包括函數內聯和函數特殊化等)。緩存
另外,全模塊優化時編譯器可以推出全部非公有(non-public)函數的使用。非公有函數僅能在模塊內部調用,因此編譯器可以肯定這些函數的全部引用。因而編譯器可以知道一個非公有函數或方法是否根本沒有被使用,從而直接刪除冗餘函數。bash
####函數特殊化舉例多線程
函數特殊化是指編譯器建立一個新版本的函數,這個函數經過一個特定的調用上下文來優化性能。在 Swift 中常見的是夠針對各類具體類型對泛型函數進行特殊化處理。架構
main.swiftapp
func add (c1: Container<Int>, c2: Container<Int>) -> Int {
return c1.getElement() + c2.getElement()
}
複製代碼
utils.swift
struct Container<T> {
var element: T
func getElement() -> T {
return element
}
}
複製代碼
單文件編譯時,當編譯器優化 main.swift 時,它並不知道 getElement
如何被實現。因此編譯器生成了一個 getElement
的調用。另外一個方面,當編譯器優化 utils.swift 時,它並不知道函數被調用了哪一個具體的類型。因此它只能生成一個通用版本的函數,這比具體類型特殊化過的代碼慢不少。
即便簡單的在 getElement
中聲明返回值,編譯器也須要在類型的元數據中查找來解決如何拷貝元素。它有多是簡單的 Int
類型,但它也能夠是一個複雜的類型,甚至涉及一些引用計數操做。而在單文件編譯的狀況下,編譯器都無從得知,更沒法優化。
而在全模塊編譯時,編譯器可以對範型函數進行函數特殊化:
utils.swift
struct Container {
var element: Int
func getElement() -> Int {
return element
}
}
複製代碼
將全部 getElement
函數被調用的地方都進行特殊化以後,函數的範型版本就能夠被刪除。這樣,使用特殊化以後的 getElement
函數,編譯器就能夠進行進一步的優化。
狀態欄 -> Editor -> Build Setting -> Add User-Defined Settings,而後增長 key 爲 SWIFT_WHOLE_MODULE_OPTIMIZATION
,value 爲 YES
就能夠了。
Swift 默認設置是 Debug 時只編譯 active 架構,Build active architecture only,Xcode 默認就是這個設置。能夠在 Build Settings --> Build active architecture only 中檢查到這一設置。
也就是說,在對每個文件單獨進行編譯時,編譯器會緩存每一個文件編譯後的產物。這樣的好處在於,若是以前編譯過了一次,以後只改動了少部分文件的內容,影響範圍不大,那麼其餘文件就不用從新編譯,速度就會很快。
而咱們來看一看全模塊優化的總體過程,包括:分析程序,類型檢查,SIL 優化,LLVM 後端。而大多數狀況下,前兩項都是很是快速的。SIL 優化主要進行的是上文所說的函數內聯、函數特殊化等優化,LLVM 後端採用多線程的方式對 SIL 優化的結果進行編譯,生成底層代碼。
而設置 SWIFT_WHOLE_MODULE_OPTIMIZATION = YES
,全模塊優化會讓增量編譯的顆粒度從 File 級別增大到 Module 級別。一個只要修改咱們項目裏的一個文件,想要編譯 debug 一下,就又得從新合併文件從頭開始編譯一次。理論上講,若是單個 LLVM 線程沒有被修改,那麼也能利用以前的緩存進行加速。但現實狀況是,分析程序、類型檢查、SIL 優化確定會被從新執行一次,而絕大部分狀況下 LLVM 也基本得從新執行一次,和第一次編譯時間差很少。
不過注意,pod 裏的庫,storyboard 和 xib 文件是不會受影響的。
dSYM 文件存儲了 debug 的一些信息,裏面包含着 crash 的信息,像 Fabric 能夠自動的將 project 中的 dSYM 文件進行解析。
新項目的默認設置是 Debug 配置編譯時不生成 dSYM 文件。有時候爲了在開發時進行 Crash 日誌解析,會去修改這個參數。生成 dSYM 會消耗大量時間,若是不須要的話,能夠去 Debug Information Format 修改一下。DWARF 是默認的不生成 dSYM 文件,DWARF with dSYM file 是會生成 dSYM 文件。
在 Xcode 9 中,蘋果官方悄悄引入了一個新的編譯系統,你能夠在 Github 中找到這一個項目。這還只是一個預覽版,因此並無在 Xcode 中默認開啓。官方新系統會改變 Swift 中處理對象間依賴的方式,旨在提升編譯速度。不過如今還不完善,有可能致使寫代碼時的詭異行爲以及較長的編譯時間。果真,我試了一下確實比原來還要慢。
若是想要開啓試試的話,能夠在 **File菜單 -> Working space ** Building System -> New Building System(Preview)
Generate dSYM | Who Module Optimization | 增長空行後第二次編譯 | 首次編譯 | 使用 New Build System | 編譯總時間 |
---|---|---|---|---|---|
✔ | ✔ | 8m 42s | |||
✔ | 8m 18s | ||||
✔ | ✔ | ✔ | 2m 2s | ||
✔ | ✔ | 1m 36s | |||
✔ | ✔ | 0m 38s | |||
✔ | 0m 16s | ||||
✔ | ✔ | ✔ | 1m 26s | ||
✔ | ✔ | 0m 55s | |||
✔ | ✔ | 9m 24s | |||
✔ | ✔ | ✔ | 1m 46s |
let array = ["a", "b", "c", "d", "e", "f", "g"]
複製代碼
這種寫法會更簡潔,可是編譯器須要進行類型推斷才能知道 array
的準確類型,因此最好的方法是直接寫出類型,避免推斷。
let array: [String] = ["a", "b", "c", "d", "e", "f", "g"]
複製代碼
let letter = someBoolean ? "a" : "b"
複製代碼
三目運算符寫法更加簡潔,但會增長編譯時間,若是想要減小編譯時間,能夠改寫爲下面的寫法。
var letter = ""
if someBoolean {
letter = "a"
} else {
letter = "b"
}
複製代碼
let string = optionalString ?? ""
複製代碼
這是 Swift 中的特殊語法,在使用 optional 類型時能夠經過這樣的方式設置 default value。可是這種寫法本質上也是三目運算符。
let string = optionalString != nil ? optionalString! : nil
複製代碼
因此,若是以節約編譯時間爲目的,也能夠改寫爲
if let string = optionalString{
print("\(string)")
} else {
print("")
}
複製代碼
let totalString = "A" + stringB + "C"
複製代碼
這樣拼接字符串可行,可是 Swift 編譯器並不青睞這樣的寫法,儘可能改寫成下面的方式。
let totalString = "A\(stringB)C"
複製代碼
let StringA = String(IntA)
複製代碼
這樣拼接字符串可行,可是 Swift 編譯器並不青睞這樣的寫法,儘可能改寫成下面的方式。
let StringA = "\(IntA)"
複製代碼
if time > 14 * 24 * 60 * 60 {}
複製代碼
這樣寫可讀性會更好,可是會對編譯器形成極大的負擔。能夠將具體內容寫在註釋中,這樣改寫:
if time > 1209600 {} // 14 * 24 * 60 * 60
複製代碼
在一個文件中,共減小了 2 處類型推斷,一共優化 0.3ms,改進效果以下:
-- | 總時間 |
---|---|
更改前 | 135.3 ms |
更改後 | 135.0 ms |
所見 Xcode 對類型推斷的處理優化仍是效果很不錯的,並且在聲明階段的類型推斷實際上並非很困難,所以提早聲明類型其實對編譯時間的優化效果影響不大。
在一個文件中,共減小了 2 處使用三目運算符的地方,一共優化 51.2ms,改進效果以下:
-- | 總時間 |
---|---|
更改前 | 229.2 ms |
更改後 | 178.0 ms |
可見使用三目運算符的地方會對編譯速度產生必定的影響,所以在不是特別須要的時候,出於編譯時間的考慮能夠改寫爲 if-else 語句。
在一個文件中,共減小了 5 處使用 nil coalescing operator 的地方,一共優化 2.8ms,具體改進效果以下:
-- | 總時間 |
---|---|
更改前 | 386.4 ms |
更改後 | 178.0 ms |
根據結果而言,優化效果並不顯著。但是根據前文所述,nil coalescing operator 其實是基於三目運算符的,那麼爲什麼優化效果反而不如三目運算符?據我推測,緣由可能在於三目運算符只須要改寫爲 if-else 語句便可,而 nil coalescing operator 大部分時候須要先用 var 實現賦值語句,在使用 if-else 對賦值進行更改,因此總的來講優化效果不大。
在一個文件中,共改進了 7 處字符串的拼接方式,一共優化 73ms,具體改進效果以下:
-- | 總時間 |
---|---|
更改前 | 696.1 ms |
更改後 | 623.1 ms |
可見改進字符串的拼接方式效果仍是十分明顯的,並且也更符合 Swift 的語法規範,因此何樂而不爲呢?
在一個文件中,進行了 5 處修改,一共優化 4952.5ms,效果十分顯著。具體改進效果以下:
-- | 總時間 |
---|---|
更改前 | 5106.2 ms |
更改後 | 153.7 ms |
在一個文件中,進行了以前例子中的修改,一共優化 843.2ms,效果十分顯著。具體改進效果以下:
-- | 總時間 |
---|---|
更改前 | 1034.7 ms |
更改後 | 191.5 ms |