美美今天請(qǐng)來了我們技術(shù)團(tuán)隊(duì)很厲害的iOS女神亞男小姐姐深度剖析Swift,她特別講解了如何才能開發(fā)出高性能的Swift程序。希望對(duì)你有所幫助哦~Enjoy Reading!
創(chuàng)新互聯(lián)專注于天橋網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠為您提供天橋營銷型網(wǎng)站建設(shè),天橋網(wǎng)站制作、天橋網(wǎng)頁設(shè)計(jì)、天橋網(wǎng)站官網(wǎng)定制、微信小程序服務(wù),打造天橋網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供天橋網(wǎng)站排名全網(wǎng)營銷落地服務(wù)。簡介
2014年,蘋果公司在WWDC上發(fā)布Swift這一新的編程語言。經(jīng)過幾年的發(fā)展,Swift已經(jīng)成為iOS開發(fā)語言的“中流砥柱”,Swift提供了非常靈活的高級(jí)別特性,例如協(xié)議、閉包、泛型等,并且Swift還進(jìn)一步開發(fā)了強(qiáng)大的SIL(Swift Intermediate Language)用于對(duì)編譯器進(jìn)行優(yōu)化,使得Swift相比Objective-C運(yùn)行更快性能更優(yōu),Swift內(nèi)部如何實(shí)現(xiàn)性能的優(yōu)化,我們本文就進(jìn)行一下解讀,希望能對(duì)大家有所啟發(fā)和幫助。
針對(duì)Swift性能提升這一問題,我們可以從概念上拆分為兩個(gè)部分:
編譯器:Swift編譯器進(jìn)行的性能優(yōu)化,從階段分為編譯期和運(yùn)行期,內(nèi)容分為時(shí)間優(yōu)化和空間優(yōu)化。
開發(fā)者:通過使用合適的數(shù)據(jù)結(jié)構(gòu)和關(guān)鍵字,幫助編譯器獲取更多信息,進(jìn)行優(yōu)化。
下面我們將從這兩個(gè)角度切入,對(duì)Swift性能優(yōu)化進(jìn)行分析。通過了解編譯器對(duì)不同數(shù)據(jù)結(jié)構(gòu)處理的內(nèi)部實(shí)現(xiàn),來選擇最合適的算法機(jī)制,并利用編譯器的優(yōu)化特性,編寫高性能的程序。
理解Swift的性能,首先要清楚Swift的數(shù)據(jù)結(jié)構(gòu),組件關(guān)系和編譯運(yùn)行方式。
數(shù)據(jù)結(jié)構(gòu)
Swift的數(shù)據(jù)結(jié)構(gòu)可以大體拆分為:Class,Struct,Enum。
組件關(guān)系
組件關(guān)系可以分為:inheritance,protocols,generics。
方法分派方式
方法分派方式可以分為Static dispatch和Dynamic dispatch。
要在開發(fā)中提高Swift性能,需要開發(fā)者去了解這幾種數(shù)據(jù)結(jié)構(gòu)和組件關(guān)系以及它們的內(nèi)部實(shí)現(xiàn),從而通過選擇最合適的抽象機(jī)制來提升性能。
首先我們對(duì)于性能標(biāo)準(zhǔn)進(jìn)行一個(gè)概念陳述,性能標(biāo)準(zhǔn)涵蓋三個(gè)標(biāo)準(zhǔn):
Allocation
Reference counting
Method dispatch
接下來,我們會(huì)分別對(duì)這幾個(gè)指標(biāo)進(jìn)行說明。
內(nèi)存分配可以分為堆區(qū)棧區(qū),在棧的內(nèi)存分配速度要高于堆,結(jié)構(gòu)體和類在堆棧分配是不同的。
Stack
基本數(shù)據(jù)類型和結(jié)構(gòu)體默認(rèn)在棧區(qū),棧區(qū)內(nèi)存是連續(xù)的,通過出棧入棧進(jìn)行分配和銷毀,速度很快,高于堆區(qū)。
我們通過一些例子進(jìn)行說明:
//示例 1
// Allocation
// Struct
struct Point {
var x, y:Double
func draw() { … }
}
let point1 = Point(x:0, y:0) //進(jìn)行point1初始化,開辟棧內(nèi)存
var point2 = point1 //初始化point2,拷貝point1內(nèi)容,開辟新內(nèi)存
point2.x = 5 //對(duì)point2的操作不會(huì)影響point1
// use `point1`
// use `point2`
以上結(jié)構(gòu)體的內(nèi)存是在棧區(qū)分配的,內(nèi)部的變量也是內(nèi)聯(lián)在棧區(qū)。將point1
賦值給point2
實(shí)際操作是在棧區(qū)進(jìn)行了一份拷貝,產(chǎn)生了新的內(nèi)存消耗point2
,這使得point1
和point2
是完全獨(dú)立的兩個(gè)實(shí)例,它們之間的操作互不影響。在使用point1
和point2
之后,會(huì)進(jìn)行銷毀。
Heap
高級(jí)的數(shù)據(jù)結(jié)構(gòu),比如類,分配在堆區(qū)。初始化時(shí)查找沒有使用的內(nèi)存塊,銷毀時(shí)再從內(nèi)存塊中清除。因?yàn)槎褏^(qū)可能存在多線程的操作問題,為了保證線程安全,需要進(jìn)行加鎖操作,因此也是一種性能消耗。
// Allocation
// Class
class Point {
var x, y:Double
func draw() { … }
}
let point1 = Point(x:0, y:0) //在堆區(qū)分配內(nèi)存,棧區(qū)只是存儲(chǔ)地址指針
let point2 = point1 //不產(chǎn)生新的實(shí)例,而是對(duì)point2增加對(duì)堆區(qū)內(nèi)存引用的指針
point2.x = 5 //因?yàn)閜oint1和point2是一個(gè)實(shí)例,所以point1的值也會(huì)被修改
// use `point1`
// use `point2`
以上我們初始化了一個(gè)Class
類型,在棧區(qū)分配一塊內(nèi)存,但是和結(jié)構(gòu)體直接在棧內(nèi)存儲(chǔ)數(shù)值不同,我們只在棧區(qū)存儲(chǔ)了對(duì)象的指針,指針指向的對(duì)象的內(nèi)存是分配在堆區(qū)的。需要注意的是,為了管理對(duì)象內(nèi)存,在堆區(qū)初始化時(shí),除了分配屬性內(nèi)存(這里是Double類型的x,y),還會(huì)有額外的兩個(gè)字段,分別是type
和refCount
,這個(gè)包含了type
,refCount
和實(shí)際屬性的結(jié)構(gòu)被稱為blue box
。
內(nèi)存分配總結(jié)
從初始化角度,Class
相比Struct
需要在堆區(qū)分配內(nèi)存,進(jìn)行內(nèi)存管理,使用了指針,有更強(qiáng)大的特性,但是性能較低。
優(yōu)化方式:
對(duì)于頻繁操作(比如通信軟件的內(nèi)容氣泡展示),盡量使用Struct
替代Class
,因?yàn)闂?nèi)存分配更快,更安全,操作更快。
Swift通過引用計(jì)數(shù)管理堆對(duì)象內(nèi)存,當(dāng)引用計(jì)數(shù)為0時(shí),Swift確認(rèn)沒有對(duì)象再引用該內(nèi)存,所以將內(nèi)存釋放。
對(duì)于引用計(jì)數(shù)的管理是一個(gè)非常高頻的間接操作,并且需要考慮線程安全,使得引用計(jì)數(shù)的操作需要較高的性能消耗。
對(duì)于基本數(shù)據(jù)類型的Struct
來說,沒有堆內(nèi)存分配和引用計(jì)數(shù)的管理,性能更高更安全,但是對(duì)于復(fù)雜的結(jié)構(gòu)體,如:
// Reference Counting
// Struct containing references
struct Label {
var text:String
var font:UIFont
func draw() { … }
}
let label1 = Label(text:"Hi", font:font) //棧區(qū)包含了存儲(chǔ)在堆區(qū)的指針
let label2 = label1 //label2產(chǎn)生新的指針,和label1一樣指向同樣的string和font地址
// use `label1`
// use `label2`
這里看到,包含了引用的結(jié)構(gòu)體相比Class
,需要管理雙倍的引用計(jì)數(shù)。每次將結(jié)構(gòu)體作為參數(shù)傳遞給方法或者進(jìn)行直接拷貝時(shí),都會(huì)出現(xiàn)多份引用計(jì)數(shù)。下圖可以比較直觀的理解:
Class在拷貝時(shí)的處理方式:
引用計(jì)數(shù)總結(jié)
Class
在堆區(qū)分配內(nèi)存,需要使用引用計(jì)數(shù)器進(jìn)行內(nèi)存管理。
基本類型的Struct
在棧區(qū)分配內(nèi)存,無引用計(jì)數(shù)管理。
包含強(qiáng)類型的Struct
通過指針管理在堆區(qū)的屬性,對(duì)結(jié)構(gòu)體的拷貝會(huì)創(chuàng)建新的棧內(nèi)存,創(chuàng)建多份引用的指針,Class
只會(huì)有一份。
優(yōu)化方式
在使用結(jié)構(gòu)體時(shí):
通過使用精確類型,例如UUID替代String(UUID字節(jié)長度固定128字節(jié),而不是String任意長度),這樣就可以進(jìn)行內(nèi)存內(nèi)聯(lián),在棧內(nèi)存儲(chǔ)UUID,我們知道,棧內(nèi)存管理更快更安全,并且不需要引用計(jì)數(shù)。
Enum替代String,在棧內(nèi)管理內(nèi)存,無引用計(jì)數(shù),并且從語法上對(duì)于開發(fā)者更友好。
我們之前在Static dispatch VS Dynamic dispatch中提到過,能夠在編譯期確定執(zhí)行方法的方式叫做靜態(tài)分派Static dispatch,無法在編譯期確定,只能在運(yùn)行時(shí)去確定執(zhí)行方法的分派方式叫做動(dòng)態(tài)分派Dynamic dispatch。
Static dispatch
更快,而且靜態(tài)分派可以進(jìn)行內(nèi)聯(lián)等進(jìn)一步的優(yōu)化,使得執(zhí)行更快速,性能更高。
但是對(duì)于多態(tài)的情況,我們不能在編譯期確定最終的類型,這里就用到了Dynamic dispatch
動(dòng)態(tài)分派。動(dòng)態(tài)分派的實(shí)現(xiàn)是,每種類型都會(huì)創(chuàng)建一張表,表內(nèi)是一個(gè)包含了方法指針的數(shù)組。動(dòng)態(tài)分派更靈活,但是因?yàn)橛胁楸砗吞D(zhuǎn)的操作,并且因?yàn)楹芏嗵攸c(diǎn)對(duì)于編譯器來說并不明確,所以相當(dāng)于block了編譯器的一些后期優(yōu)化。所以速度慢于Static dispatch
。
下面看一段多態(tài)代碼,以及分析實(shí)現(xiàn)方式:
//引用語義實(shí)現(xiàn)的多態(tài)
class Drawable { func draw() {} }
class Point :Drawable {
var x, y:Double
override func draw() { … }
}
class Line :Drawable {
var x1, y1, x2, y2:Double
override func draw() { … }
}
var drawables:[Drawable]
for d in drawables {
d.draw()
}
Method Dispatch總結(jié)
Class
默認(rèn)使用Dynamic dispatch
,因?yàn)樵诰幾g期幾乎每個(gè)環(huán)節(jié)的信息都無法確定,所以阻礙了編譯器的優(yōu)化,比如inline
和whole module inline
。
使用Static dispatch代替Dynamic dispatch提升性能
我們知道Static dispatch
快于Dynamic dispatch
,如何在開發(fā)中去盡可能使用Static dispatch
。
inheritance constraints
繼承約束
我們可以使用final
關(guān)鍵字去修飾Class
,以此生成的Final class
,使用Static dispatch
。
access control
訪問控制private
關(guān)鍵字修飾,使得方法或?qū)傩灾粚?duì)當(dāng)前類可見。編譯器會(huì)對(duì)方法進(jìn)行Static dispatch
。
編譯器可以通過whole module optimization
檢查繼承關(guān)系,對(duì)某些沒有標(biāo)記final
的類通過計(jì)算,如果能在編譯期確定執(zhí)行的方法,則使用Static dispatch
。Struct
默認(rèn)使用Static dispatch
。
Swift快于OC的一個(gè)關(guān)鍵是可以消解動(dòng)態(tài)分派。
總結(jié)
Swift提供了更靈活的Struct
,用以在內(nèi)存、引用計(jì)數(shù)、方法分派等角度去進(jìn)行性能的優(yōu)化,在正確的時(shí)機(jī)選擇正確的數(shù)據(jù)結(jié)構(gòu),可以使我們的代碼性能更快更安全。
延伸
你可能會(huì)問Struct
如何實(shí)現(xiàn)多態(tài)呢?答案是protocol oriented programming
。
以上分析了影響性能的幾個(gè)標(biāo)準(zhǔn),那么不同的算法機(jī)制Class
,Protocol Types
和Generic code
,它們?cè)谶@三方面的表現(xiàn)如何,Protocol Type
和Generic code
分別是怎么實(shí)現(xiàn)的呢?我們帶著這個(gè)問題看下去。
這里我們會(huì)討論P(yáng)rotocol Type如何存儲(chǔ)和拷貝變量,以及方法分派是如何實(shí)現(xiàn)的。不通過繼承或者引用語義的多態(tài):
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é)議的類型集合,可能是point或者line
for d in drawables {
d.draw()
}
以上通過Protocol Type
實(shí)現(xiàn)多態(tài),幾個(gè)類之間沒有繼承關(guān)系,故不能按照慣例借助V-Table
實(shí)現(xiàn)動(dòng)態(tài)分派。
如果想了解Vtable和Witness table實(shí)現(xiàn),可以進(jìn)行點(diǎn)擊查看,這里不做細(xì)節(jié)說明。
因?yàn)镻oint和Line的尺寸不同,數(shù)組存儲(chǔ)數(shù)據(jù)實(shí)現(xiàn)一致性存儲(chǔ),使用了Existential Container
。查找正確的執(zhí)行方法則使用了 Protoloc Witness Table
。
Existential Container
是一種特殊的內(nèi)存布局方式,用于管理遵守了相同協(xié)議的數(shù)據(jù)類型Protocol Type
,這些數(shù)據(jù)類型因?yàn)椴还蚕硗焕^承關(guān)系(這是V-Table
實(shí)現(xiàn)的前提),并且內(nèi)存空間尺寸不同,使用Existential Container
進(jìn)行管理,使其具有存儲(chǔ)的一致性。
結(jié)構(gòu)如下:
三個(gè)詞大小的valueBuffer
這里介紹一下valueBuffer結(jié)構(gòu),valueBuffer有三個(gè)詞,每個(gè)詞包含8個(gè)字節(jié),存儲(chǔ)的可能是值,也可能是對(duì)象的指針。對(duì)于small value(空間小于valueBuffer),直接存儲(chǔ)在valueBuffer的地址內(nèi), inline valueBuffer,無額外堆內(nèi)存初始化。當(dāng)值的數(shù)量大于3個(gè)屬性即large value,或者總尺寸超過valueBuffer的占位,就會(huì)在堆區(qū)開辟內(nèi)存,將其存儲(chǔ)在堆區(qū),valueBuffer存儲(chǔ)內(nèi)存指針。
value witness table的引用
因?yàn)?code>Protocol Type的類型不同,內(nèi)存空間,初始化方法等都不相同,為了對(duì)Protocol Type
生命周期進(jìn)行專項(xiàng)管理,用到了Value Witness Table
。
protocol witness table的引用
管理Protocol Type
的方法分派。
內(nèi)存分布如下:
1. payload_data_0 = 0x0000000000000004,
2. payload_data_1 = 0x0000000000000000,
3. payload_data_2 = 0x0000000000000000,
4. instance_type = 0x000000010d6dc408 ExistentialContainers`type
metadata for ExistentialContainers.Car,
5. protocol_witness_0 = 0x000000010d6dc1c0
ExistentialContainers protocol witness table for
ExistentialContainers.Car:ExistentialContainers.Drivable
in ExistentialContainers
為了實(shí)現(xiàn)Class
多態(tài)也就是引用語義多態(tài),需要V-Table
來實(shí)現(xiàn),但是V-Table
的前提是具有同一個(gè)父類即共享相同的繼承關(guān)系,但是對(duì)于Protocol Type
來說,并不具備此特征,故為了支持Struct
的多態(tài),需要用到protocol oriented programming
機(jī)制,也就是借助Protocol Witness Table
來實(shí)現(xiàn)(細(xì)節(jié)可以點(diǎn)擊Vtable和witness table實(shí)現(xiàn),每個(gè)結(jié)構(gòu)體會(huì)創(chuàng)造PWT
表,內(nèi)部包含指針,指向方法具體實(shí)現(xiàn))。
用于管理任意值的初始化、拷貝、銷毀。
Value Witness Table
的結(jié)構(gòu)如上,是用于管理遵守了協(xié)議的Protocol Type
實(shí)例的初始化,拷貝,內(nèi)存消減和銷毀的。
Value Witness Table
在SIL
中還可以拆分為%relative_vwtable
和%absolute_vwtable
,我們這里先不做展開。
Value Witness Table
和Protocol Witness Table
通過分工,去管理Protocol Type
實(shí)例的內(nèi)存管理(初始化,拷貝,銷毀)和方法調(diào)用。
我們來借助具體的示例進(jìn)行進(jìn)一步了解:
// Protocol Types
// The Existential Container in action
func drawACopy(local :Drawable) {
local.draw()
}
let val :Drawable = Point()
drawACopy(val)
在Swift編譯器中,通過Existential Container
實(shí)現(xiàn)的偽代碼如下:
// Protocol Types
// The Existential Container in action
func drawACopy(local :Drawable) {
local.draw()
}
let val :Drawable = Point()
drawACopy(val)
//existential container的偽代碼結(jié)構(gòu)
struct ExistContDrawable {
var valueBuffer:(Int, Int, Int)
var vwt:ValueWitnessTable
var pwt:DrawableProtocolWitnessTable
}
// drawACopy方法生成的偽代碼
func drawACopy(val:ExistContDrawable) { //將existential container傳入
var local = ExistContDrawable() //初始化container
let vwt = val.vwt //獲取value witness table,用于管理生命周期
let pwt = val.pwt //獲取protocol witness table,用于進(jìn)行方法分派
local.type = type
local.pwt = pwt
vwt.allocateBufferAndCopyValue(&local, val) //vwt進(jìn)行生命周期管理,初始化或者拷貝
pwt.draw(vwt.projectBuffer(&local)) //pwt查找方法,這里說一下projectBuffer,因?yàn)椴煌愋驮趦?nèi)存中是不同的(small value內(nèi)聯(lián)在棧內(nèi),large value初始化在堆內(nèi),棧持有指針),所以方法的確定也是和類型相關(guān)的,我們知道,查找方法時(shí)是通過當(dāng)前對(duì)象的地址,通過一定的位移去查找方法地址。
vwt.destructAndDeallocateBuffer(temp) //vwt進(jìn)行生命周期管理,銷毀內(nèi)存
}
我們知道,Swift中Class
的實(shí)例和屬性都存儲(chǔ)在堆區(qū),Struct
實(shí)例在棧區(qū),如果包含指針屬性則存儲(chǔ)在堆區(qū),Protocol Type
如何存儲(chǔ)屬性?Small Number通過Existential Container
內(nèi)聯(lián)實(shí)現(xiàn),大數(shù)存在堆區(qū)。如何處理Copy呢?
在出現(xiàn)Copy情況時(shí):
let aLine = Line(1.0, 1.0, 1.0, 3.0)
let pair = Pair(aLine, aLine)
let copy = pair
會(huì)將新的Exsitential Container
的valueBuffer指向同一個(gè)value即創(chuàng)建指針引用,但是如果要改變值怎么辦?我們知道Struct
值的修改和Class
不同,Copy是不應(yīng)該影響原實(shí)例的值的。
這里用到了一個(gè)技術(shù)叫做Indirect Storage With Copy-On-Write
,即優(yōu)先使用內(nèi)存指針。通過提高內(nèi)存指針的使用,來降低堆區(qū)內(nèi)存的初始化。降低內(nèi)存消耗。在需要修改值的時(shí)候,會(huì)先檢測(cè)引用計(jì)數(shù)檢測(cè),如果有大于1的引用計(jì)數(shù),則開辟新內(nèi)存,創(chuàng)建新的實(shí)例。在對(duì)內(nèi)容進(jìn)行變更的時(shí)候,會(huì)開啟一塊新的內(nèi)存,偽代碼如下:
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) { //如何存在多份引用,則開啟新內(nèi)存,否則直接修改
storage = LineStorage(storage)
}
storage。start = ...
}
}
這樣實(shí)現(xiàn)的目的:通過多份指針去引用同一份地址的成本遠(yuǎn)遠(yuǎn)低于開辟多份堆內(nèi)存。以下對(duì)比圖:
支持Protocol Type的動(dòng)態(tài)多態(tài)(Dynamic Polymorphism)行為。
通過使用Witness Table和Existential Container來實(shí)現(xiàn)。
對(duì)于大數(shù)的拷貝可以通過Indirect Storage間接存儲(chǔ)來進(jìn)行優(yōu)化。
說到動(dòng)態(tài)多態(tài)Dynamic Polymorphism
,我們就要問了,什么是靜態(tài)多態(tài)Static Polymorphism
,看看下面示例:
// Drawing a copy
protocol Drawable {
func draw()
}
func drawACopy(local :Drawable) {
local.draw()
}
let line = Line()
drawACopy(line)
// ...
let point = Point()
drawACopy(point)
這種情況我們就可以用到泛型Generic code
來實(shí)現(xiàn),進(jìn)行進(jìn)一步優(yōu)化。
我們接下來會(huì)討論泛型屬性的存儲(chǔ)方式和泛型方法是如何分派的。泛型和Protocol Type
的區(qū)別在于:
泛型支持的是靜態(tài)多態(tài)。
每個(gè)調(diào)用上下文只有一種類型。
查看下面的示例,foo
和bar
方法是同一種類型。
在調(diào)用鏈中會(huì)通過類型降級(jí)進(jìn)行類型取代。
對(duì)于以下示例:
func foo(local :T) {
bar(local)
}
func bar(local:T) { … }
let point = Point()
foo(point)
分析方法foo
和bar
的調(diào)用過程:
//調(diào)用過程
foo(point)-->foo(point) //在方法執(zhí)行時(shí),Swift將泛型T綁定為調(diào)用方使用的具體類型,這里為Point
bar(local) -->bar(local) //在調(diào)用內(nèi)部bar方法時(shí),會(huì)使用foo已經(jīng)綁定的變量類型Point,可以看到,泛型T在這里已經(jīng)被降級(jí),通過類型Point進(jìn)行取代
泛型方法調(diào)用的具體實(shí)現(xiàn)為:
同一種類型的任何實(shí)例,都共享同樣的實(shí)現(xiàn),即使用同一個(gè)Protocol Witness Table。
使用Protocol/Value Witness Table。
每個(gè)調(diào)用上下文只有一種類型:這里沒有使用Existential Container
, 而是將Protocol/Value Witness Table
作為調(diào)用方的額外參數(shù)進(jìn)行傳遞。
變量初始化和方法調(diào)用,都使用傳入的VWT
和PWT
來執(zhí)行。
看到這里,我們并不覺得泛型比Protocol Type
有什么更快的特性,泛型如何更快呢?靜態(tài)多態(tài)前提下可以進(jìn)行進(jìn)一步的優(yōu)化,稱為特定泛型優(yōu)化。
靜態(tài)多態(tài):在調(diào)用站中只有一種類型
Swift使用只有一種類型的特點(diǎn),來進(jìn)行類型降級(jí)取代。
類型降級(jí)后,產(chǎn)生特定類型的方法
為泛型的每個(gè)類型創(chuàng)造對(duì)應(yīng)的方法
這時(shí)候你可能會(huì)問,那每一種類型都產(chǎn)生一個(gè)新的方法,代碼空間豈不爆炸?
靜態(tài)多態(tài)下進(jìn)行特定優(yōu)化specialization
因?yàn)槭庆o態(tài)多態(tài)。所以可以進(jìn)行很強(qiáng)大的優(yōu)化,比如進(jìn)行內(nèi)聯(lián)實(shí)現(xiàn),并且通過獲取上下文來進(jìn)行更進(jìn)一步的優(yōu)化。從而降低方法數(shù)量。優(yōu)化后可以更精確和具體。
例如:
func min(x:T, y:T) -> T {
return y < x ? y : x
}
從普通的泛型展開如下,因?yàn)橐С炙蓄愋偷?code>min方法,所以需要對(duì)泛型類型進(jìn)行計(jì)算,包括初始化地址、內(nèi)存分配、生命周期管理等。除了對(duì)value的操作,還要對(duì)方法進(jìn)行操作。這是一個(gè)非常的的工程。
func min(x:T, y:T, FTable:FunctionTable) -> T {
let xCopy = FTable.copy(x)
let yCopy = FTable.copy(y)
let m = FTable.lessThan(yCopy, xCopy) ? y :x
FTable.release(x)
FTable.release(y)
return m
}
在確定入?yún)㈩愋蜁r(shí),比如Int,編譯器可以通過泛型特化,進(jìn)行類型取代(Type Substitute),優(yōu)化為:
func min(x:Int, y:Int) -> Int {
return y < x ? y :x
}
泛型特化specilization
是何時(shí)發(fā)生的?
在使用特定優(yōu)化時(shí),調(diào)用方需要進(jìn)行類型推斷,這里需要知曉類型的上下文,例如類型的定義和內(nèi)部方法實(shí)現(xiàn)。如果調(diào)用方和類型是單獨(dú)編譯的,就無法在調(diào)用方推斷類型的內(nèi)部實(shí)行,就無法使用特定優(yōu)化,保證這些代碼一起進(jìn)行編譯,這里就用到了whole module optimization
。而whole module optimization
是對(duì)于調(diào)用方和被調(diào)用方的方法在不同文件時(shí),對(duì)其進(jìn)行泛型特化優(yōu)化的前提。
特定泛型的進(jìn)一步優(yōu)化:
// Pairs in our program using generic types
struct Pair {
init(_ f:T, _ s:T) {
first = f ; second = s
}
var first:T
var second:T
}
let pairOfLines = Pair(Line(), Line())
// ...
let pairOfPoint = Pair(Point(), Point())
在用到多種泛型,且確定泛型類型不會(huì)在運(yùn)行時(shí)修改時(shí),就可以對(duì)成對(duì)泛型的使用進(jìn)行進(jìn)一步優(yōu)化。
優(yōu)化的方式是將泛型的內(nèi)存分配由指針指定,變?yōu)閮?nèi)存內(nèi)聯(lián),不再有額外的堆初始化消耗。請(qǐng)注意,因?yàn)檫M(jìn)行了存儲(chǔ)內(nèi)聯(lián),已經(jīng)確定了泛型特定類型的內(nèi)存分布,泛型的內(nèi)存內(nèi)聯(lián)不能存儲(chǔ)不同類型。所以再次強(qiáng)調(diào)此種優(yōu)化只適用于在運(yùn)行時(shí)不會(huì)修改泛型類型,即不能同時(shí)支持一個(gè)方法中包含line
和point
兩種類型。
whole module optimization
是用于Swift編譯器的優(yōu)化機(jī)制。可以通過-whole-module-optimization
(或 -wmo
)進(jìn)行打開。在XCode 8之后默認(rèn)打開。 Swift Package Manager
在release模式默認(rèn)使用whole module optimization
。
module是多個(gè)文件集合。
編譯器在對(duì)源文件進(jìn)行語法分析之后,會(huì)對(duì)其進(jìn)行優(yōu)化,生成機(jī)器碼并輸出目標(biāo)文件,之后鏈接器聯(lián)合所有的目標(biāo)文件生成共享庫或可執(zhí)行文件。
whole module optimization
通過跨函數(shù)優(yōu)化,可以進(jìn)行內(nèi)聯(lián)等優(yōu)化操作,對(duì)于泛型,可以通過獲取類型的具體實(shí)現(xiàn)來進(jìn)行推斷優(yōu)化,進(jìn)行類型降級(jí)方法內(nèi)聯(lián),刪除多余方法等操作。
全模塊優(yōu)化的優(yōu)勢(shì)
編譯器掌握所有方法的實(shí)現(xiàn),可以進(jìn)行內(nèi)聯(lián)和泛型特化等優(yōu)化,通過計(jì)算所有方法的引用,移除多余的引用計(jì)數(shù)操作。
通過知曉所有的非公共方法,如果這寫方法沒有被使用,就可以對(duì)其進(jìn)行消除。
如何降低編譯時(shí)間
和全模塊優(yōu)化相反的是文件優(yōu)化,即對(duì)單個(gè)文件進(jìn)行編譯。這樣的好處在于可以并行執(zhí)行,并且對(duì)于沒有修改的文件不會(huì)再次編譯。缺點(diǎn)在于編譯器無法獲知全貌,無法進(jìn)行深度優(yōu)化,全模塊優(yōu)化如何避免沒修改的文件再次編譯。
編譯器內(nèi)部運(yùn)行過程分為:語法分析,類型檢查,SIL
優(yōu)化,LLVM
后端處理。
語法分析和類型檢查一般很快,SIL
優(yōu)化執(zhí)行了重要的Swift特定優(yōu)化,例如泛型特化和方法內(nèi)聯(lián)等,該過程大概占用真?zhèn)€編譯時(shí)間的三分之一。LLVM
后端執(zhí)行占用了大部分的編譯時(shí)間,用于運(yùn)行降級(jí)優(yōu)化和生成代碼。
進(jìn)行全模塊優(yōu)化后,SIL
優(yōu)化會(huì)將模塊再次拆分為多個(gè)部分,LLVM
后端通過多線程對(duì)這些拆分模塊進(jìn)行處理,對(duì)于沒有修改的部分,不會(huì)進(jìn)行再處理。這樣就避免了修改一小部分,整個(gè)大模塊進(jìn)行LLVM
后端執(zhí)行,并且多線程并行操作也會(huì)縮短處理時(shí)間。
Swift因?yàn)榉椒ǚ峙蓹C(jī)制問題,所以在設(shè)計(jì)和優(yōu)化后,會(huì)產(chǎn)生和我們常規(guī)理解不太一致的結(jié)果,這當(dāng)然不能算Bug。但是還是要單獨(dú)進(jìn)行說明,避免在開發(fā)過程中,因?yàn)閷?duì)機(jī)制的掌握不足,造成預(yù)期和執(zhí)行出入導(dǎo)致的問題。
Message dispatch
我們通過上面說明結(jié)合Static dispatch VS Dynamic dispatch對(duì)方法分派方式有了了解。這里需要對(duì)Objective-C
的方法分派方式進(jìn)行說明。
熟悉OC的人都知道,OC采用了運(yùn)行時(shí)機(jī)制使用obj_msgSend
發(fā)送消息,runtime非常的靈活,我們不僅可以對(duì)方法調(diào)用采用swizzling
,對(duì)于對(duì)象也可以通過isa-swizzling
來擴(kuò)展功能,應(yīng)用場(chǎng)景有我們常用的hook和大家熟知的KVO
。
大家在使用Swift進(jìn)行開發(fā)時(shí)都會(huì)問,Swift是否可以使用OC的運(yùn)行時(shí)和消息轉(zhuǎn)發(fā)機(jī)制呢?答案是可以。
Swift可以通過關(guān)鍵字dynamic
對(duì)方法進(jìn)行標(biāo)記,這樣就會(huì)告訴編譯器,此方法使用的是OC的運(yùn)行時(shí)機(jī)制。
注意:我們常見的關(guān)鍵字
@ObjC
并不會(huì)改變Swift原有的方法分派機(jī)制,關(guān)鍵字@ObjC
的作用只是告訴編譯器,該段代碼對(duì)于OC可見。
總結(jié)來說,Swift通過dynamic
關(guān)鍵字的擴(kuò)展后,一共包含三種方法分派方式:Static dispatch
,Table dispatch
和Message dispatch
。下表為不同的數(shù)據(jù)結(jié)構(gòu)在不同情況下采取的分派方式:
如果在開發(fā)過程中,錯(cuò)誤的混合了這幾種分派方式,就可能出現(xiàn)Bug,以下我們對(duì)這些Bug進(jìn)行分析:
SR-584
此情況是在子類的extension中重載父類方法時(shí),出現(xiàn)和預(yù)期不同的行為。
class Base:NSObject {
var directProperty:String { return "This is Base" }
var indirectProperty:String { return directProperty }
}
class Sub:Base { }
extension Sub {
override var directProperty:String { return "This is Sub" }
}
執(zhí)行以下代碼,直接調(diào)用沒有問題:
Base().directProperty // “This is Base”
Sub().directProperty // “This is Sub”
間接調(diào)用結(jié)果和預(yù)期不同:
Base()。indirectProperty // “This is Base”
Sub()。indirectProperty // expected "this is Sub",but is “This is Base” <- Unexpected!
在Base.directProperty
前添加dynamic
關(guān)鍵字就可以獲得"this is Sub"的結(jié)果。Swift在extension 文檔中說明,不能在extension中重載已經(jīng)存在的方法。
“Extensions can add new functionality to a type, but they cannot override existing functionality.”
會(huì)出現(xiàn)警告:Cannot override a non-dynamic class declaration from an extension
。
出現(xiàn)這個(gè)問題的原因是,NSObject的extension是使用的Message dispatch
,而Initial Declaration
使用的是Table dispath
(查看上圖 Swift Dispatch Method)。extension重載的方法添加在了Message dispatch
內(nèi),沒有修改虛函數(shù)表,虛函數(shù)表內(nèi)還是父類的方法,故會(huì)執(zhí)行父類方法。想在extension重載方法,需要標(biāo)明dynamic
來使用Message dispatch
。
SR-103
協(xié)議的擴(kuò)展內(nèi)實(shí)現(xiàn)的方法,無法被遵守類的子類重載:
protocol Greetable {
func sayHi()
}
extension Greetable {
func sayHi() {
print("Hello")
}
}
func greetings(greeter:Greetable) {
greeter.sayHi()
}
現(xiàn)在定義一個(gè)遵守了協(xié)議的類Person
。遵守協(xié)議類的子類LoudPerson
:
class Person:Greetable {
}
class LoudPerson:Person {
func sayHi() {
print("sub")
}
}
執(zhí)行下面代碼結(jié)果為:
var sub:LoudPerson = LoudPerson()
sub.sayHi() //sub
不符合預(yù)期的代碼:
var sub:Person = LoudPerson()
sub.sayHi() //HellO <-使用了protocol的默認(rèn)實(shí)現(xiàn)
注意,在子類LoudPerson
中沒有出現(xiàn)override
關(guān)鍵字。可以理解為LoudPerson
并沒有成功注冊(cè)Greetable
在Witness table
的方法。所以對(duì)于聲明為Person
實(shí)際為LoudPerson
的實(shí)例,會(huì)在編譯器通過Person
去查找,Person
沒有實(shí)現(xiàn)協(xié)議方法,則不產(chǎn)生Witness table
,sayHi
方法是直接調(diào)用的。解決辦法是在base類內(nèi)實(shí)現(xiàn)協(xié)議方法,無需實(shí)現(xiàn)也要提供默認(rèn)方法?;蛘邔⒒悩?biāo)記為final
來避免繼承。
進(jìn)一步通過示例去理解:
// Defined protocol。
protocol A {
func a() -> Int
}
extension A {
func a() -> Int {
return 0
}
}
// A class doesn't have implement of the function。
class B:A {}
class C:B {
func a() -> Int {
return 1
}
}
// A class has implement of the function。
class D:A {
func a() -> Int {
return 1
}
}
class E:D {
override func a() -> Int {
return 2
}
}
// Failure cases。
B().a() // 0
C().a() // 1
(C() as A).a() // 0 # We thought return 1。
// Success cases。
D().a() // 1
(D() as A).a() // 1
E().a() // 2
(E() as A).a() // 2
其他
我們知道Class extension使用的是Static dispatch:
class MyClass {
}
extension MyClass {
func extensionMethod() {}
}
class SubClass:MyClass {
override func extensionMethod() {}
}
以上代碼會(huì)出現(xiàn)錯(cuò)誤,提示Declarations in extensions can not be overridden yet
。
影響程序的性能標(biāo)準(zhǔn)有三種:初始化方式, 引用指針和方法分派。
文中對(duì)比了兩種數(shù)據(jù)結(jié)構(gòu):Struct
和Class
的在不同標(biāo)準(zhǔn)下的性能表現(xiàn)。Swift相比OC和其它語言強(qiáng)化了結(jié)構(gòu)體的能力,所以在了解以上性能表現(xiàn)的前提下,通過利用結(jié)構(gòu)體可以有效提升性能。
在此基礎(chǔ)上,我們還介紹了功能強(qiáng)大的結(jié)構(gòu)體的類:Protocol Type
和Generic
。并且介紹了它們?nèi)绾沃С侄鄳B(tài)以及通過使用有條件限制的泛型如何讓程序更快。
swift memorylayout
witness table video
protocol types pdf
protocol and value oriented programming in UIKit apps video
optimizing swift performance
whole module optimizaiton
increasing performance by reducing dynamic dispatch
protocols generics existential container
protocols and generics
why swift is swift
swift method dispatch
swift extension
universal dynamic dispatch for method calls
compiler performance.md
structures and classes
作者簡介
亞男,美團(tuán)點(diǎn)評(píng)iOS工程師。2017年加入美團(tuán)點(diǎn)評(píng),負(fù)責(zé)美團(tuán)管家開發(fā),研究編譯器原理。目前正積極推動(dòng)Swift組件化建設(shè)。
歡迎加入美團(tuán)iOS技術(shù)交流群,跟作者零距離交流。進(jìn)群方式:請(qǐng)加美美同學(xué)的微信(微信號(hào):MTDPtech01),回復(fù):iOS,美美會(huì)自動(dòng)拉你進(jìn)群。
---------- END ----------
招聘信息
我們餐飲生態(tài)技術(shù)部是一個(gè)技術(shù)氛圍活躍,大牛聚集的地方。新到店緊握真正的大規(guī)模SaaS實(shí)戰(zhàn)機(jī)會(huì),多租戶、數(shù)據(jù)、安全、開放平臺(tái)等全方位的挑戰(zhàn)。業(yè)務(wù)領(lǐng)域復(fù)雜技術(shù)挑戰(zhàn)多,技術(shù)和業(yè)務(wù)能力迅速提升,最重要的是,加入我們,你將實(shí)現(xiàn)真正通過代碼來改變行業(yè)的夢(mèng)想。我們歡迎各端人才加入,Java優(yōu)先。感興趣的同學(xué)趕緊發(fā)送簡歷至 zhaoyanan02@meituan.com,我們期待你的到來。
原文鏈接:https://mp.weixin.qq.com/s/U95QmOOjeXkk-yC23cuZCQ