轉(zhuǎn)自深入剖析Swift性能優(yōu)化,我為該文作者,現(xiàn)使用簡書平臺(tái)發(fā)布。
簡介
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的性能,首先要清楚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)行說明。
Allocation
內(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// Structstruct 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// Classclass 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)存分配更快,更安全,操作更快。
Reference counting
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 referencesstruct 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ù)。下圖可以比較直觀的理解:
備注:包含引用類型的結(jié)構(gòu)體出現(xiàn)Copy的處理方式
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ā)者更友好。
Method Dispatch
我們之前在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è)問題看下去。
Protocol Type
這里我們會(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或者linefor 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
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
Protocol Witness Table(PWT)
為了實(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(VWT)
用于管理任意值的初始化、拷貝、銷毀。
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 actionfunc drawACopy(local :Drawable) { local.draw()}let val :Drawable = Point()drawACopy(val)
在Swift編譯器中,通過Existential Container
實(shí)現(xiàn)的偽代碼如下:
// Protocol Types// The Existential Container in actionfunc 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)存}
Protocol Type 存儲(chǔ)屬性
我們知道,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呢?
Protocol大數(shù)的Copy優(yōu)化
在出現(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多態(tài)總結(jié)
支持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 copyprotocol 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<T:Drawable>(local :T) { bar(local)}func bar<T:Drawable>(local:T) { … }let point = Point()foo(point)
分析方法foo
和bar
的調(diào)用過程:
//調(diào)用過程foo(point)-->foo<T = Point>(point) //在方法執(zhí)行時(shí),Swift將泛型T綁定為調(diào)用方使用的具體類型,這里為Point bar(local) -->bar<T = Point>(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<T:Comparable>(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<T:Comparable>(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<Int>(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)化
特定泛型的進(jìn)一步優(yōu)化:
// Pairs in our program using generic typesstruct Pair<T :Drawable> { 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
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í)間。
擴(kuò)展:Swift的隱藏“Bug”
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)行分析:
此情況是在子類的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
。
協(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() // 0C().a() // 1(C() as A).a() // 0 # We thought return 1。 // Success cases。D().a() // 1(D() as A).a() // 1E().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
。
總結(jié)
影響程序的性能標(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)以及通過使用有條件限制的泛型如何讓程序更快。
參考資料
作者簡介
亞男,美團(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,我們期待你的到來。