下面這篇文檔收集了一系列編寫高性能 Swift 代碼的要訣和技巧。文檔的目標讀者是編譯器和標準庫開發(fā)人員。
文檔中的一些技巧可以幫助提升您的 Swift 程序質(zhì)量,使您的代碼不容易出錯且可讀性更好。顯式地標記最終類和類協(xié)議是兩個顯而易見的例子。 然而文檔中還有一些技巧是不符合規(guī)矩的,扭曲的,僅僅解決一些比編譯器或語言的特殊的臨時性需求。文檔中的很多建議來自于多方面的權(quán)衡,例如:運行時、字節(jié)大小、代碼可讀性等等。
啟用優(yōu)化
第一個應該做的事情就是啟用優(yōu)化。Swift 提供了三種不同的優(yōu)化級別:
-Onone: 這意味著正常的開發(fā)。它執(zhí)行最小優(yōu)化和保存所有調(diào)試信息。
-O: 這意味著對于大多數(shù)生產(chǎn)代碼。編譯器執(zhí)行積極地優(yōu)化,可以大大改變提交代碼的類型和數(shù)量。調(diào)試信息將被省略但還是會有損害的。
-Ounchecked: 這是一個特殊的優(yōu)化模式,它意味著特定的庫或應用程序,這是以安全性來交換的。編譯器將刪除所有溢出檢查以及一些隱式類型檢查。這不是在通常情況下使用的,因為它可能會導致內(nèi)存安全問題和整數(shù)溢出。如果你仔細審查你的代碼,那么對整數(shù)溢出和類型轉(zhuǎn)換來說是安全的。
在 Xcode UI 中,可以修改的當前優(yōu)化級別如下:
...
整個組件優(yōu)化
默認情況下 Swift 單獨編譯每個文件。這使得 Xcode 可以非??焖俚牟⑿芯幾g多個文件。然而,分開編譯每個文件可以預防某些編譯器優(yōu)化。Swift 也可以猶如它是一個文件一樣編譯整個程序,猶如就好像它是一個單一的編譯單元一樣優(yōu)化這個程序。這個模式可以使用命令行 flag-whole-module-optimization 來激活。在這種模式下編譯的程序?qū)⒆钭钣锌赡苄枰L時間來編譯,單可以運行得更快。
這個模式可以通過 XCode 構(gòu)建設置中的“Whole Module Optimization”來激活。
降低動態(tài)調(diào)度
Swift 在默認情況下是一個類似 Objective-C 的非常動態(tài)的語言。與 Objective-C 不同的是,Swift 給了程序員通過消除和減少這種特性來提供運行時性能的能力。本節(jié)提供幾個可被用于這樣的操作的語言結(jié)構(gòu)的例子。
動態(tài)調(diào)度
類使用動態(tài)調(diào)度的方法和默認的屬性訪問。因此在下面的代碼片段中,a.aProperty、a.doSomething() 和 a.doSomethingElse() 都將通過動態(tài)調(diào)度來調(diào)用:
class A { var aProperty: [Int] func doSomething() { ... } dynamic doSomethingElse() { ... } } class B : A { override var aProperty { get { ... } set { ... } } override func doSomething() { ... } } func usingAnA(a: A) { a.doSomething() a.aProperty = ... }
在 Swift 中,動態(tài)調(diào)度默認通過一個 vtable[1](虛函數(shù)表)間接調(diào)用。如果使用一個 dynamic 關(guān)鍵字來聲明,Swift 將會通過調(diào)用 Objective-C 通知來發(fā)送呼叫代替。這兩種情況中,這種情況會比直接的函數(shù)調(diào)用較慢,因為它防止了對間接呼叫本身之外程序開銷的許多編譯器優(yōu)化[2]。在性能關(guān)鍵的代碼中,人們常常會想限制這種動態(tài)行為。
建議:當你知道聲明不需要被重寫時使用“final”。
final 關(guān)鍵字是一個類、一個方法、或一個屬性聲明中的一個限制,使得這樣的聲明不得被重寫。這意味著編譯器可以呼叫直接的函數(shù)調(diào)用代替間接調(diào)用。例如下面的 C.array1 和 D.array1 將會被直接[3]訪問。與之相反,D.array2 將通過一個虛函數(shù)表訪問:
final class C { // No declarations in class 'C' can be overridden. var array1: [Int] func doSomething() { ... } } class D { final var array1 [Int] // 'array1' cannot be overridden by a computed property. var array2: [Int] // 'array2' *can* be overridden by a computed property. } func usingC(c: C) { c.array1[i] = ... // Can directly access C.array without going through dynamic dispatch. c.doSomething() = ... // Can directly call C.doSomething without going through virtual dispatch. } func usingD(d: D) { d.array1[i] = ... // Can directly access D.array1 without going through dynamic dispatch. d.array2[i] = ... // Will access D.array2 through dynamic dispatch. }
建議:當聲明的東西不需要被文件外部被訪問到的時候,就用“private”
將 private 關(guān)鍵詞用在一個聲明上,會限制對其進行了聲明的文件的可見性。這會讓編輯器有能力甄別出所有其它潛在的覆蓋聲明。如此,由于沒有了任何這樣的聲明,使得編譯器可以自動地推斷出 final 關(guān)鍵詞,并據(jù)此去掉對方面的間接調(diào)用和屬性的訪問。例如在如下的 e.doSomething() 和 f.myPrivateVar 中,就將可以被直接訪問,假定在同一個文件中,E, F 并沒有任何覆蓋的聲明:
private class E { func doSomething() { ... } } class F { private var myPrivateVar : Int } func usingE(e: E) { e.doSomething() // There is no sub class in the file that declares this class. // The compiler can remove virtual calls to doSomething() // and directly call A’s doSomething method. } func usingF(f: F) -> Int { return f.myPrivateVar }
高效的使用容器類型
通用的容器 Array 和 Dictionary 是有 Swift 標準庫提供的一個重要的功能特性。本節(jié)將介紹如何用一種高性能的方式使用這些類型。
建議:在數(shù)組中使用值類型
在 Swift 中,類型可以分為不同的兩類:值類型(結(jié)構(gòu)體,枚舉,元組)和引用類型(類)。一個關(guān)鍵的區(qū)分是 NSArray 不能含有值類型。因此當使用值類型時,優(yōu)化器就不需要去處理對 NSArray 的支持,從而可以在數(shù)組上省去大部分消耗。
此外,相比引用類型,如果值類型遞歸地含有引用類型,那么值類型僅僅需要引用計數(shù)器。而如果使用沒有引用類型的值類型,就可以避免額外的開銷,從而釋放數(shù)組內(nèi)的流量。
// Don't use a class here. struct PhonebookEntry { var name : String var number : [Int] } var a : [PhonebookEntry]
記住要在使用大值類型和使用引用類型之間做好權(quán)衡。在某些情況下,拷貝和移動大值類型數(shù)據(jù)的消耗要大于移除橋接和持有/釋放的消耗。
建議:當 NSArray 橋接不必要時,使用 ContiguousArray 存儲引用類型。
如果你需要一個引用類型的數(shù)組,而且數(shù)組不需要橋接到 NSArray 時,使用 ContiguousArray 替代 Array:
class C { ... } var a: ContiguousArray = [C(...), C(...), ..., C(...)]
建議:使用適當?shù)母淖兌皇菍ο蠓峙洹?/span>
在 Swift 中所有的標準庫容器都使用 COW(copy-on-write) 執(zhí)行拷貝代替即時拷貝。在很多情況下,這可以讓編譯器通過持有容器而不是深度拷貝,從而省掉不必要的拷貝。如果容器的引用計數(shù)大于 1 并容器時被改變時,就會拷貝底層容器。例如:在下面這種情況:當 d 被分配給 c 時不拷貝,但是當 d 經(jīng)歷了結(jié)構(gòu)性的改變追加 2,那么 d 將會被拷貝,然后 2 被追加到 b:
var c: [Int] = [ ... ] var d = c // No copy will occur here. d.append(2) // A copy *does* occur here.如果用戶不小心時,有時 COW 會引起額外的拷貝。例如,在函數(shù)中,試圖通過對象分配執(zhí)行修改。在 Swift 中,所有的參數(shù)傳遞時都會被拷貝一份,例如,參數(shù)在調(diào)用點之前持有一份,然后在調(diào)用的函數(shù)結(jié)束時釋放。也就是說,像下面這樣的函數(shù):
func append_one(a: [Int]) -> [Int] { a.append(1) return a } var a = [1, 2, 3] a = append_one(a)盡管由于分配,a 的版本沒有任何改變 ,在 append_one 后也沒有使用 , 但 a 也許會被拷貝。這可以通過使用 inout 參數(shù)來避免這個問題:
func append_one_in_place(inout a: [Int]) { a.append(1) } var a = [1, 2, 3] append_one_in_place(&a)
未檢查操作
Swift 通過在執(zhí)行普通計算時檢查溢出的方法解決了整數(shù)溢出的 bug。這些檢查在已確定沒有內(nèi)存安全問題會發(fā)生的高效的代碼中,是不合適的。
建議:當你確切的知道不會發(fā)生溢出時使用未檢查整型計算。
在對性能要求高的代碼中,如果你知道你的代碼是安全的,那么你可以忽略溢出檢查。
a : [Int] b : [Int] c : [Int] // Precondition: for all a[i], b[i]: a[i] + b[i] does not overflow! for i in 0 ... n { c[i] = a[i] &+ b[i] }
泛型
Swift 通過泛型類型的使用,提供了一個非常強大的抽象機制 。Swift 編譯器發(fā)出一個可以對任何 T 執(zhí)行 MySwiftFunc的具體的代碼塊。生成的代碼需要一個函數(shù)指針表和一個包含 T 的盒子作為額外的參數(shù)。MySwiftFunc 和 MySwiftFunc之間的不同的行為通過傳遞不同的函數(shù)指針表和通過盒子提供的抽象大小來說明。一個泛型的例子:
class MySwiftFunc { ... } MySwiftFunc X // Will emit code that works with Int... MySwiftFunc Y // ... as well as String.當優(yōu)化器啟用時,Swift 編譯器尋找這段代碼的調(diào)用,并試著確認在調(diào)用中具體使用的類型(例如:非泛型類型)。如果泛型函數(shù)的定義對優(yōu)化器來說是可見的,并知道具體類型,Swift 編譯器將生成一個有特殊類型的特殊泛型函數(shù)。那么調(diào)用這個特殊函數(shù)的這個過程就可以避免關(guān)聯(lián)泛型的消耗。一些泛型的例子:
class MyStack { func push(element: T) { ... } func pop() -> T { ... } } func myAlgorithm(a: [T], length: Int) { ... } // The compiler can specialize code of MyStack[Int] var stackOfInts: MyStack[Int] // Use stack of ints. for i in ... { stack.push(...) stack.pop(...) } var arrayOfInts: [Int] // The compiler can emit a specialized version of 'myAlgorithm' targeted for // [Int]' types. myAlgorithm(arrayOfInts, arrayOfInts.length)
建議:將泛型的聲明放在使用它的文件中
只有在泛型聲明在當前模塊可見的情況下優(yōu)化器才能執(zhí)行特殊化。這只有在使用泛型的代碼和聲明泛型的代碼在同一個文件中才能發(fā)生。注意標準庫是一個例外。在標準庫中聲明的泛型對所有模塊可見并可以進行特殊化。
建議:允許編譯器進行特殊化
只有當調(diào)用位置和被調(diào)函數(shù)位于同一個編譯單元的時候編譯器才能對泛型代碼進行特殊化。我們可以使用一個技巧讓編譯器對被調(diào)函數(shù)進行優(yōu)化,這個技巧就是在被調(diào)函數(shù)所在的編譯單元中執(zhí)行類型檢查。執(zhí)行類型檢查的代碼會重新分發(fā)這個調(diào)用到泛型函數(shù)---可是這一次它攜帶了類型信息。在下面的代碼中,我們在函數(shù) play_a_game 中插入了類型檢查,使得代碼的速度提高了幾百倍。
//Framework.swift: protocol Pingable { func ping() -> Self } protocol Playable { func play() } extension Int : Pingable { func ping() -> Int { return self + 1 } } class Game : Playable { var t : T init (_ v : T) {t = v} func play() { for _ in 0...100_000_000 { t = t.ping() } } } func play_a_game(game : Playable ) { // This check allows the optimizer to specialize the // generic call 'play' if let z = game as? Game { z.play() } else { game.play() } } /// -------------- >8 // Application.swift: play_a_game(Game(10)
大的值對象的開銷
在 swift 語言中,值類型保存它們數(shù)據(jù)獨有的一份拷貝。使用值類型有很多優(yōu)點,比如值類型具有獨立的狀態(tài)。當我們拷貝值類型時(相當于復制,初始化參數(shù)傳遞等操作),程序會創(chuàng)建值類型的一個拷貝。對于大的值類型,這種拷貝時很耗費時間的,可能會影響到程序的性能。
讓我們看一下下面這段代碼。這段代碼使用值類型的節(jié)點定義了一個樹,樹的節(jié)點包含了協(xié)議類型的其他節(jié)點,計算機圖形場景經(jīng)常由可以使用值類型表示的實體以及形態(tài)變化,因此這個例子很有實踐意義
protocol P {} struct Node : P { var left, right : P? } struct Tree { var node : P? init() { ... } }
當樹進行拷貝時(參數(shù)傳遞,初始化或者賦值)整個樹都需要被復制.這是一項花銷很大的操作,需要很多的 malloc/free 調(diào)用以及以及大量的引用計數(shù)操作
然而,我們并不關(guān)系值是否被拷貝,只要在這些值還在內(nèi)存中存在就可以。
建議:對大的值類型使用 COW(copy-on-write,寫時復制和數(shù)組有點類似)
減少復制大的值類型數(shù)據(jù)開銷的辦法時采用寫時復制行為(當對象改變時才進行實際的復制工作)。最簡單的實現(xiàn)寫時復制的方案時使用已經(jīng)存在的寫時復制的數(shù)據(jù)結(jié)構(gòu),比如數(shù)組。Swift 的數(shù)據(jù)是值類型,但是當數(shù)組作為參數(shù)被傳遞時并不每次都進行復制,因為它具有寫時復制的特性。
在我們的 Tree 的例子中我們通過將 tree 的內(nèi)容包裝成一個數(shù)組來減少復制的代價。這個簡單的改變對我們 tree 數(shù)據(jù)結(jié)構(gòu)的性能影響時巨大的,作為參數(shù)傳遞數(shù)組的代價從 O(n) 變?yōu)?O(1)。
struct tree : P { var node : [P?] init() { node = [ thing ] } }
但是使用數(shù)組實現(xiàn) COW 機制有兩個明顯的不足,第一個問題是數(shù)組暴露的諸如 append 以及 count 之類的方法在值包裝的上下文中沒有任何作用,這些方法使得引用類型的封裝變得棘手。也許我們可以通過創(chuàng)建一個封裝的結(jié)構(gòu)體并隱藏這些不用的 API 來解決這個問題,但是卻無法解決第二個問題。第二個問題就是數(shù)組內(nèi)部存在保證程序安全性的代碼以及和 OC 交互的代碼。Swift 要檢查給出的下表是否摟在數(shù)組的邊界內(nèi),當保存值的時候需要檢查是否需要擴充存儲空間。這些運行時檢查會降低速度。
一個替代的方案是實現(xiàn)一個專門的使用 COW 機制的數(shù)據(jù)結(jié)構(gòu)代替采用數(shù)組作為值的封裝。構(gòu)建這樣一個數(shù)據(jù)結(jié)構(gòu)的示例如下所示:
final class Ref { var val : T init(_ v : T) {val = v} } struct Box { var ref : Ref init(_ x : T) { ref = Ref(x) } var value: T { get { return ref.val } set { if (!isUniquelyReferencedNonObjC(&ref)) { ref = Ref(newValue) return } ref.val = newValue } } }
類型 Box 可以代替上個例子中的數(shù)組
不安全的代碼
Swift 語言的類都是采用引用計數(shù)進行內(nèi)存管理的。Swift 編譯器會在每次對象被訪問的時候插入增加引用計數(shù)的代碼。例如,考慮一個遍歷使用類實現(xiàn)的一個鏈表的例子。遍歷鏈表是通過移動引用到鏈表的下一個節(jié)點來完成的:elem = elem.next,每次移動這個引用,Swift 都要增加 next 對象的引用計數(shù)并減少前一個對象的引用計數(shù),這種引用計數(shù)代價昂貴但是只要使用 swift 類就無法避免
final class Node { var next: Node? var data: Int ... }
建議:使用未托管的引用避免引用計數(shù)的負荷
在效率至上的代碼中你可以選擇使用未托管的引用。Unmanaged結(jié)構(gòu)體允許開發(fā)者對特別的引用關(guān)閉引用計數(shù)
var Ref : Unmanaged = Unmanaged.passUnretained(Head) while let Next = Ref.takeUnretainedValue().next { ... Ref = Unmanaged.passUnretained(Next) }
協(xié)議
建議:將只有類實現(xiàn)的協(xié)議標記為類協(xié)議
Swift 可以指定協(xié)議只能由類實現(xiàn)。標記協(xié)議只能由類實現(xiàn)的一個好處是編譯器可以基于這一點對程序進行優(yōu)化。例如,ARC 內(nèi)存管理系統(tǒng)能夠容易的持有(增加該對象的引用計數(shù))如果它知道它正在處理一個類對象。如果編譯器不知道這一點,它就必須假設結(jié)構(gòu)體也可以實現(xiàn)協(xié)議,那么它就必須準備好持有或者釋放不同的數(shù)據(jù)結(jié)構(gòu),而這代價將會十分昂貴。
如果限制只能由類實現(xiàn)某協(xié)議那么就標記該協(xié)議為類協(xié)議以獲得更好的性能
protocol Pingable : class { func ping() -> Int }
腳注
【1】虛擬方法表或者 vtable 是被一個實例引用的一種包含類型方法地址的類型約束表。進行動態(tài)分發(fā)時,首先從對象中查找這張表然后查找表中的方法
【2】這是因為編譯器并不知道那個具體的方法要被調(diào)用
【3】例如,直接加載一個類的字段或者直接調(diào)用一個方法
【4】解釋 COW 是什么
【5】在特定情況下優(yōu)化器能夠通過內(nèi)聯(lián)和 ARC 優(yōu)化技術(shù)移除 retain,release 因為沒有引起復制