原文連接git
前一篇文章《Swift 性能優化(1)——基本概念》中咱們提到了編程語言的派發方式,Swift 支持文中所提到的三種派發方式。其中,函數表派發是 Swift OOP 的底層支持,那麼,Swift POP 以及泛型編程底層又是如何實現的呢?github
本文,咱們就來簡單探討一下協議和泛型的底層實現原理。若是想深刻學習協議和泛型的更多細節和原理,建議去學習一下 Swift Intermediate Language 相關的內容。之後要是有時間,我也想去學習瞭解一下 SIL。編程
首先咱們舉一個例子來看一下 OOP 是如何實現多態的。swift
class Drawable { func draw() }
class Point : Drawable {
var x, y:Double
func draw() { ... }
}
class Line : Drawable {
var x1, y1, x2, y2:Double
func draw() { ... }
}
let point = Point(x: 0, y: 0)
let line = Line(x1: 0, y1: 0, x2: 1, y2: 1)
var drawables: [Drawable] = [point, line]
for d in drawables {
d.draw()
}
複製代碼
從上述代碼能夠看出,變量 drawables
是一個元素類型爲 Drawable
的數組,因爲 class
關鍵字標記了 Drawable
及其子類 Point
、Line
都是引用類型,所以 drawables
的內存佈局是固定的,數組裏的每個元素都是一個指針。以下圖所示。數組
接下來,咱們再來看 OOP 是如何經過 virtual table
來實現動態派發的。以下圖所示性能優化
運行時執行 d.draw()
,會根據 d
所指向的對象的 type
字段索引到該類型所對應的函數表,最終調用正確的方法。bash
下面咱們舉一個例子看一下 POP 是如何實現多態的。app
protocol Drawable { func draw() }
struct Point : Drawable {
var x, y: Double
func draw() { ... }
}
struct Line : Drawable {
var x1, y1, x2, y2: Double
func draw() { ... }
}
class SharedLine: Drawable {
var x1, y1, x2, y2: Double
func draw() { ... }
}
let point = Point(x: 0, y: 0)
let line = Line(x1: 0, y1: 0, x2: 1, y2: 1)
let sharedLine = SharedLine(x1: 0, y1: 0, x2: 1, y2: 1)
var drawables: [Drawable] = [point, line, sharedLine]
for d in drawables {
d.draw()
}
複製代碼
須要注意的是,此時 Point
和 Line
都是值類型的 struct
,只有 SharedLine
是引用類型的 class
,而且 Drawable
再也不是一個基類,而是一個 協議類型(Protocol Type)。編程語言
那麼此時,變量 drawables
的內存佈局是怎樣呢?畢竟,運行時 d
多是遵循協議的任意類型,類型不一樣,內存大小也會不一樣。函數
事實上,在這種狀況下,變量 drawables
中存儲的元素是一種特殊的數據類型:Existential Container。
Existential Container 是編譯器生成的一種特殊的數據類型,用於管理遵照了相同協議的協議類型。由於這些數據類型的內存空間尺寸不一樣,使用 Extential Container 進行管理能夠實現存儲一致性。
咱們在上述代碼的基礎上執行下面的示例代碼。
let point = Point(x: 0, y: 0)
let line = Line(x1: 0, y1: 0, x2: 1, y2: 1)
let sharedLine = SharedLine(x1: 0, y1: 0, x2: 1, y2: 1)
print("\(MemoryLayout.size(ofValue: point))")
print("\(MemoryLayout.size(ofValue: line))")
print("\(MemoryLayout.size(ofValue: sharedLine))")
var drawables: [Drawable] = [point, line, sharedLine]
for d in drawables {
print("\(MemoryLayout.size(ofValue: d))")
}
// 原始類型的內存大小,單位:字節
16
32
8
// 協議類型的內存大小,單位:字節
40
40
40
複製代碼
因爲本機內存對齊是 8 字節,可見 Extension Container
類型佔據 5 個內存單元(也稱 詞,Word)。其結構以下圖所示:
下面,咱們依次進行介紹。
Value Buffer 佔據 3 個詞,存儲的多是值,也多是指針。對於 Small Value(存儲空間小於等於 Value Buffer),能夠直接內聯存儲在 Value Buffer 中。對於 Large Value(存儲空間大於 Value Buffer),則會在堆區分配內存進行存儲,Value Buffer 只存儲對應的指針。以下圖所示。
因爲協議類型的具體類型不一樣,其內存佈局也不一樣,Value Witness Table 則是對協議類型的生命週期進行專項管理,從而處理具體類型的初始化、拷貝、銷燬。以下圖所示:
Value Witness Table 管理協議類型的生命週期,Protocol Witness Table 則管理協議類型的方法調用。
在 OOP 中,基於繼承關係的多態是經過 Virtual Table 實現的;在 POP 中,沒有繼承關係,由於沒法使用 Virtual Table 實現基於協議的多態,取而代之的是 Protocol Witness Table。
注:關於 Virtual Table 和 Protocol Witness Table 的區別,個人理解是:
它們都是一個記錄函數地址的列表(即函數表),只是它們的生成方式是不一樣的。
對於 Virtual Table,在編譯時,子類的函數表是經過對基類函數表進行拷貝、覆寫、插入等操做生成的。
對於 Protocol Witness Table,在編譯時,函數表是經過檢查具體類型對協議的實現,直接生成的。
由上述 Value Buffer 相關內容可知,協議類型的存儲分兩種狀況
那麼,協議類型的存儲屬性是如何拷貝的呢?事實上,對於 Small Value,就是直接拷貝 Existential Container,值也內聯在其中。可是,對於 Large Value,Swift 採用了 Indirect Storage With Copy-On-Write 技術進行了優化。
這種技術能夠提升內存指針利用率,下降堆區內存消耗,從而實現性能提高。該技術的原理是:拷貝時僅僅拷貝 Extension Container,當修改值時,先檢測引用計數,若是引用計數大於 1,則開闢新的堆區內存。其實現僞代碼以下所示:
class LineStorage {
var x1, y1, x2, y2:Double
}
struct Line : Drawable {
var storage : LineStorage
init() { storage = LineStorage(Point(), Point()) }
func draw() { … }
mutating func move() {
if !isUniquelyReferencedNonObjc(&storage) {
// 若是存在多份引用,則開啓新內存,不然直接修改
storage = LineStorage(storage)
}
storage.start = ...
}
}
複製代碼
下面,咱們來討論泛型的實現。首先來看一個例子。
func foo<T: Drawable>(local: T) {
bar(local)
}
func bar<T: Drawable>(local: T) {
}
let point = Point()
foo(point)
複製代碼
上述代碼中,泛型方法的調用過程大概以下:
// foo 方法執行時,Swift 將泛型 T 綁定爲具體類型。示例中是 Point
foo(point) --> foo<T = Point>(point)
// 調用內部 bar 方法時,Swift 會使用已綁定的變量類型 Point 進一步綁定到 bar 方法的泛型 T 上。
bar(local) --> bar<T = Point>(local)
複製代碼
相比協議類型而言,泛型類型在調用時老是能肯定類型,所以無需使用 Existential Container。在調用泛型方法時,只須要將 Value Witness Table/Protocol Witness Table 做爲額外參數進行傳遞。
注:根據方法調用時數據類型是否肯定能夠將多態分爲:靜態多態(Static Polymorphism)和 動態多態(Dynamic Polymorphism)。
在泛型類型調用方法時, Swift 會將泛型綁定爲具體的類型。所以泛型實現的是靜態多態。
在協議類型調用方法時,類型是 Existential Container,須要在方法內部進一步根據 pwt 進行方法索引。所以協議實現的是動態多態。
咱們以一個例子來講明編譯器對於泛型的一種優化技術:泛型特化。
func min<T: Comparable>(x: T, y: T) -> T {
return y < x ? y : x
}
let a: Int = 1
let b: Int = 2
min(a, b)
複製代碼
上述代碼,編譯器在編譯期間就能經過類型推導肯定調用 min()
方法時的類型。此時,編譯器就會經過泛型特化,進行 類型取代(Type Substitute),生成以下的一個方法:
func min<Int>(x: Int, y: Int) -> Int {
return y < x ? y :x
}
複製代碼
泛型特化會爲每一個類型生成一個對應的方法。那麼是否是會出現代碼空間爆炸的狀況呢?事實上,並不會出現這種狀況。由於編譯器能夠進行代碼內聯以及進一步的優化,從而下降方法數量並提升性能。
泛型特化的前提是編譯器在編譯期間能夠進行類型推導,這就要求在編譯時提供類型的上下文。若是調用方和類型是單獨編譯的,就沒法在編譯時進行類型推導,所以沒法使用泛型特化。爲了可以在編譯期間提供完整的上下文,咱們能夠經過 全模塊優化(Whole Module Optimization) 編譯選項,實現調用方和類型在不一樣文件時也能進行泛型特化。
全模塊優化是用於 Swift 編譯器的優化機制。從 Xcode 8 開始默認開啓。
本文,咱們瞭解了協議類型和泛型類型對於多態的實現,從中咱們也看到了編譯器對於 Swift 性能的優化發揮了巨大的做用,如:泛型特化、生成代碼實現 Copy-On-Write。
此外,咱們瞭解了關於泛型和協議關於性能優化的啓示,可以咱們制定技術方案時進行權衡。