轉(zhuǎn)自深入剖析Swift性能優(yōu)化,我為該文作者乃沙,現(xiàn)使用簡書平臺發(fā)布抒线。
簡介
2014年粟耻,蘋果公司在WWDC上發(fā)布Swift這一新的編程語言垢乙。經(jīng)過幾年的發(fā)展锨咙,Swift已經(jīng)成為iOS開發(fā)語言的“中流砥柱”,Swift提供了非常靈活的高級別特性追逮,例如協(xié)議酪刀、閉包粹舵、泛型等,并且Swift還進一步開發(fā)了強大的SIL(Swift Intermediate Language)用于對編譯器進行優(yōu)化骂倘,使得Swift相比Objective-C運行更快性能更優(yōu)眼滤,Swift內(nèi)部如何實現(xiàn)性能的優(yōu)化,我們本文就進行一下解讀历涝,希望能對大家有所啟發(fā)和幫助诅需。
針對Swift性能提升這一問題,我們可以從概念上拆分為兩個部分:
編譯器:Swift編譯器進行的性能優(yōu)化荧库,從階段分為編譯期和運行期堰塌,內(nèi)容分為時間優(yōu)化和空間優(yōu)化。
開發(fā)者:通過使用合適的數(shù)據(jù)結(jié)構(gòu)和關鍵字电爹,幫助編譯器獲取更多信息蔫仙,進行優(yōu)化。
下面我們將從這兩個角度切入丐箩,對Swift性能優(yōu)化進行分析摇邦。通過了解編譯器對不同數(shù)據(jù)結(jié)構(gòu)處理的內(nèi)部實現(xiàn),來選擇最合適的算法機制屎勘,并利用編譯器的優(yōu)化特性施籍,編寫高性能的程序。
理解Swift的性能
理解Swift的性能概漱,首先要清楚Swift的數(shù)據(jù)結(jié)構(gòu)丑慎,組件關系和編譯運行方式。
-
數(shù)據(jù)結(jié)構(gòu)
Swift的數(shù)據(jù)結(jié)構(gòu)可以大體拆分為:Class瓤摧,Struct竿裂,Enum。
-
組件關系
組件關系可以分為:inheritance照弥,protocols腻异,generics。
-
方法分派方式
方法分派方式可以分為Static dispatch和Dynamic dispatch这揣。
要在開發(fā)中提高Swift性能悔常,需要開發(fā)者去了解這幾種數(shù)據(jù)結(jié)構(gòu)和組件關系以及它們的內(nèi)部實現(xiàn),從而通過選擇最合適的抽象機制來提升性能给赞。
首先我們對于性能標準進行一個概念陳述机打,性能標準涵蓋三個標準:
Allocation
Reference counting
Method dispatch
接下來,我們會分別對這幾個指標進行說明片迅。
Allocation
內(nèi)存分配可以分為堆區(qū)棧區(qū)残邀,在棧的內(nèi)存分配速度要高于堆,結(jié)構(gòu)體和類在堆棧分配是不同的。
Stack
基本數(shù)據(jù)類型和結(jié)構(gòu)體默認在棧區(qū)罐旗,棧區(qū)內(nèi)存是連續(xù)的膳汪,通過出棧入棧進行分配和銷毀,速度很快九秀,高于堆區(qū)。
我們通過一些例子進行說明:
//示例 1// Allocation// Structstruct Point { var x, y:Double func draw() { … }}let point1 = Point(x:0, y:0) //進行point1初始化粘我,開辟棧內(nèi)存var point2 = point1 //初始化point2鼓蜒,拷貝point1內(nèi)容,開辟新內(nèi)存point2.x = 5 //對point2的操作不會影響point1// use `point1`// use `point2`
以上結(jié)構(gòu)體的內(nèi)存是在棧區(qū)分配的征字,內(nèi)部的變量也是內(nèi)聯(lián)在棧區(qū)都弹。將point1
賦值給point2
實際操作是在棧區(qū)進行了一份拷貝析命,產(chǎn)生了新的內(nèi)存消耗point2
渐溶,這使得point1
和point2
是完全獨立的兩個實例,它們之間的操作互不影響元莫。在使用point1
和point2
之后氮昧,會進行銷毀框杜。
Heap
高級的數(shù)據(jù)結(jié)構(gòu),比如類袖肥,分配在堆區(qū)咪辱。初始化時查找沒有使用的內(nèi)存塊,銷毀時再從內(nèi)存塊中清除椎组。因為堆區(qū)可能存在多線程的操作問題油狂,為了保證線程安全,需要進行加鎖操作寸癌,因此也是一種性能消耗专筷。
// Allocation// Classclass Point { var x, y:Double func draw() { … }}let point1 = Point(x:0, y:0) //在堆區(qū)分配內(nèi)存,棧區(qū)只是存儲地址指針let point2 = point1 //不產(chǎn)生新的實例蒸苇,而是對point2增加對堆區(qū)內(nèi)存引用的指針point2.x = 5 //因為point1和point2是一個實例磷蛹,所以point1的值也會被修改// use `point1`// use `point2`
以上我們初始化了一個Class
類型,在棧區(qū)分配一塊內(nèi)存填渠,但是和結(jié)構(gòu)體直接在棧內(nèi)存儲數(shù)值不同弦聂,我們只在棧區(qū)存儲了對象的指針,指針指向的對象的內(nèi)存是分配在堆區(qū)的氛什。需要注意的是莺葫,為了管理對象內(nèi)存,在堆區(qū)初始化時枪眉,除了分配屬性內(nèi)存(這里是Double類型的x捺檬,y),還會有額外的兩個字段贸铜,分別是type
和refCount
堡纬,這個包含了type
聂受,refCount
和實際屬性的結(jié)構(gòu)被稱為blue box
。
內(nèi)存分配總結(jié)
從初始化角度烤镐,Class
相比Struct
需要在堆區(qū)分配內(nèi)存蛋济,進行內(nèi)存管理,使用了指針炮叶,有更強大的特性碗旅,但是性能較低。
優(yōu)化方式:
對于頻繁操作(比如通信軟件的內(nèi)容氣泡展示)镜悉,盡量使用Struct
替代Class
祟辟,因為棧內(nèi)存分配更快,更安全侣肄,操作更快旧困。
Reference counting
Swift通過引用計數(shù)管理堆對象內(nèi)存,當引用計數(shù)為0時稼锅,Swift確認沒有對象再引用該內(nèi)存吼具,所以將內(nèi)存釋放。
對于引用計數(shù)的管理是一個非常高頻的間接操作缰贝,并且需要考慮線程安全馍悟,使得引用計數(shù)的操作需要較高的性能消耗。
對于基本數(shù)據(jù)類型的Struct
來說剩晴,沒有堆內(nèi)存分配和引用計數(shù)的管理锣咒,性能更高更安全,但是對于復雜的結(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ū)包含了存儲在堆區(qū)的指針let label2 = label1 //label2產(chǎn)生新的指針毅整,和label1一樣指向同樣的string和font地址// use `label1`// use `label2`
看到,包含了引用的結(jié)構(gòu)體相比Class
绽左,需要管理雙倍的引用計數(shù)悼嫉。每次將結(jié)構(gòu)體作為參數(shù)傳遞給方法或者進行直接拷貝時,都會出現(xiàn)多份引用計數(shù)拼窥。下圖可以比較直觀的理解:
備注:包含引用類型的結(jié)構(gòu)體出現(xiàn)Copy的處理方式
Class在拷貝時的處理方式:
引用計數(shù)總結(jié)
Class
在堆區(qū)分配內(nèi)存戏蔑,需要使用引用計數(shù)器進行內(nèi)存管理。基本類型的
Struct
在棧區(qū)分配內(nèi)存鲁纠,無引用計數(shù)管理总棵。包含強類型的
Struct
通過指針管理在堆區(qū)的屬性,對結(jié)構(gòu)體的拷貝會創(chuàng)建新的棧內(nèi)存改含,創(chuàng)建多份引用的指針情龄,Class
只會有一份。
優(yōu)化方式
在使用結(jié)構(gòu)體時:
通過使用精確類型,例如UUID替代String(UUID字節(jié)長度固定128字節(jié)骤视,而不是String任意長度)鞍爱,這樣就可以進行內(nèi)存內(nèi)聯(lián),在棧內(nèi)存儲UUID专酗,我們知道睹逃,棧內(nèi)存管理更快更安全,并且不需要引用計數(shù)祷肯。
Enum替代String唯卖,在棧內(nèi)管理內(nèi)存,無引用計數(shù)躬柬,并且從語法上對于開發(fā)者更友好。
Method Dispatch
我們之前在Static dispatch VS Dynamic dispatch中提到過抽减,能夠在編譯期確定執(zhí)行方法的方式叫做靜態(tài)分派Static dispatch允青,無法在編譯期確定,只能在運行時去確定執(zhí)行方法的分派方式叫做動態(tài)分派Dynamic dispatch卵沉。
Static dispatch
更快颠锉,而且靜態(tài)分派可以進行內(nèi)聯(lián)等進一步的優(yōu)化,使得執(zhí)行更快速史汗,性能更高琼掠。
但是對于多態(tài)的情況,我們不能在編譯期確定最終的類型停撞,這里就用到了Dynamic dispatch
動態(tài)分派瓷蛙。動態(tài)分派的實現(xiàn)是,每種類型都會創(chuàng)建一張表戈毒,表內(nèi)是一個包含了方法指針的數(shù)組艰猬。動態(tài)分派更靈活,但是因為有查表和跳轉(zhuǎn)的操作埋市,并且因為很多特點對于編譯器來說并不明確冠桃,所以相當于block了編譯器的一些后期優(yōu)化。所以速度慢于Static dispatch
道宅。
下面看一段多態(tài)代碼食听,以及分析實現(xiàn)方式:
//引用語義實現(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
默認使用Dynamic dispatch
,因為在編譯期幾乎每個環(huán)節(jié)的信息都無法確定污茵,所以阻礙了編譯器的優(yōu)化樱报,比如inline
和whole module inline
。
使用Static dispatch代替Dynamic dispatch提升性能
我們知道Static dispatch
快于Dynamic dispatch
省咨,如何在開發(fā)中去盡可能使用Static dispatch
肃弟。
inheritance constraints
繼承約束
我們可以使用final
關鍵字去修飾Class
,以此生成的Final class
,使用Static dispatch
笤受。access control
訪問控制private
關鍵字修飾穷缤,使得方法或?qū)傩灾粚Ξ斍邦惪梢姟>幾g器會對方法進行Static dispatch
箩兽。
編譯器可以通過whole module optimization
檢查繼承關系津肛,對某些沒有標記final
的類通過計算,如果能在編譯期確定執(zhí)行的方法汗贫,則使用Static dispatch
身坐。Struct
默認使用Static dispatch
。
Swift快于OC的一個關鍵是可以消解動態(tài)分派落包。
總結(jié)
Swift提供了更靈活的Struct
部蛇,用以在內(nèi)存、引用計數(shù)咐蝇、方法分派等角度去進行性能的優(yōu)化涯鲁,在正確的時機選擇正確的數(shù)據(jù)結(jié)構(gòu),可以使我們的代碼性能更快更安全有序。
延伸
你可能會問Struct
如何實現(xiàn)多態(tài)呢?答案是protocol oriented programming
抹腿。
以上分析了影響性能的幾個標準,那么不同的算法機制Class
旭寿,Protocol Types
和Generic code
警绩,它們在這三方面的表現(xiàn)如何,Protocol Type
和Generic code
分別是怎么實現(xiàn)的呢盅称?我們帶著這個問題看下去肩祥。
Protocol Type
這里我們會討論Protocol Type如何存儲和拷貝變量,以及方法分派是如何實現(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
實現(xiàn)多態(tài),幾個類之間沒有繼承關系逞盆,故不能按照慣例借助V-Table
實現(xiàn)動態(tài)分派檀蹋。
如果想了解Vtable和Witness table實現(xiàn),可以進行點擊查看云芦,這里不做細節(jié)說明俯逾。
因為Point和Line的尺寸不同,數(shù)組存儲數(shù)據(jù)實現(xiàn)一致性存儲舅逸,使用了Existential Container
桌肴。查找正確的執(zhí)行方法則使用了 Protoloc Witness Table
。
Existential Container
Existential Container
是一種特殊的內(nèi)存布局方式琉历,用于管理遵守了相同協(xié)議的數(shù)據(jù)類型Protocol Type
坠七,這些數(shù)據(jù)類型因為不共享同一繼承關系(這是V-Table
實現(xiàn)的前提)水醋,并且內(nèi)存空間尺寸不同,使用Existential Container
進行管理彪置,使其具有存儲的一致性拄踪。
結(jié)構(gòu)如下:
三個詞大小的valueBuffer
這里介紹一下valueBuffer結(jié)構(gòu),valueBuffer有三個詞拳魁,每個詞包含8個字節(jié)惶桐,存儲的可能是值,也可能是對象的指針潘懊。對于small value(空間小于valueBuffer)姚糊,直接存儲在valueBuffer的地址內(nèi), inline valueBuffer授舟,無額外堆內(nèi)存初始化救恨。當值的數(shù)量大于3個屬性即large value,或者總尺寸超過valueBuffer的占位释树,就會在堆區(qū)開辟內(nèi)存忿薇,將其存儲在堆區(qū),valueBuffer存儲內(nèi)存指針躏哩。value witness table的引用
因為Protocol Type
的類型不同,內(nèi)存空間揉燃,初始化方法等都不相同扫尺,為了對Protocol Type
生命周期進行專項管理,用到了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)
為了實現(xiàn)Class
多態(tài)也就是引用語義多態(tài),需要V-Table
來實現(xiàn)抢腐,但是V-Table
的前提是具有同一個父類即共享相同的繼承關系姑曙,但是對于Protocol Type
來說,并不具備此特征迈倍,故為了支持Struct
的多態(tài)伤靠,需要用到protocol oriented programming
機制,也就是借助Protocol Witness Table
來實現(xiàn)(細節(jié)可以點擊Vtable和witness table實現(xiàn)啼染,每個結(jié)構(gòu)體會創(chuàng)造PWT
表宴合,內(nèi)部包含指針,指向方法具體實現(xiàn))迹鹅。
Value Witness Table(VWT)
用于管理任意值的初始化卦洽、拷貝、銷毀斜棚。
Value Witness Table
的結(jié)構(gòu)如上阀蒂,是用于管理遵守了協(xié)議的Protocol Type
實例的初始化该窗,拷貝,內(nèi)存消減和銷毀的蚤霞。Value Witness Table
在SIL
中還可以拆分為%relative_vwtable
和%absolute_vwtable
酗失,我們這里先不做展開。Value Witness Table
和Protocol Witness Table
通過分工争便,去管理Protocol Type
實例的內(nèi)存管理(初始化级零,拷貝,銷毀)和方法調(diào)用滞乙。
我們來借助具體的示例進行進一步了解:
// Protocol Types// The Existential Container in actionfunc drawACopy(local :Drawable) { local.draw()}let val :Drawable = Point()drawACopy(val)
在Swift編譯器中奏纪,通過Existential Container
實現(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斩启,用于進行方法分派 local.type = type local.pwt = pwt vwt.allocateBufferAndCopyValue(&local, val) //vwt進行生命周期管理序调,初始化或者拷貝 pwt.draw(vwt.projectBuffer(&local)) //pwt查找方法,這里說一下projectBuffer兔簇,因為不同類型在內(nèi)存中是不同的(small value內(nèi)聯(lián)在棧內(nèi)发绢,large value初始化在堆內(nèi),棧持有指針)垄琐,所以方法的確定也是和類型相關的边酒,我們知道,查找方法時是通過當前對象的地址狸窘,通過一定的位移去查找方法地址墩朦。 vwt.destructAndDeallocateBuffer(temp) //vwt進行生命周期管理,銷毀內(nèi)存}
Protocol Type 存儲屬性
我們知道翻擒,Swift中Class
的實例和屬性都存儲在堆區(qū)氓涣,Struct
實例在棧區(qū),如果包含指針屬性則存儲在堆區(qū)陋气,Protocol Type
如何存儲屬性劳吠?Small Number通過Existential Container
內(nèi)聯(lián)實現(xiàn),大數(shù)存在堆區(qū)巩趁。如何處理Copy呢?
Protocol大數(shù)的Copy優(yōu)化
在出現(xiàn)Copy情況時:
let aLine = Line(1.0, 1.0, 1.0, 3.0)let pair = Pair(aLine, aLine)let copy = pair
會將新的Exsitential Container
的valueBuffer指向同一個value即創(chuàng)建指針引用痒玩,但是如果要改變值怎么辦?我們知道Struct
值的修改和Class
不同,Copy是不應該影響原實例的值的议慰。
這里用到了一個技術(shù)叫做Indirect Storage With Copy-On-Write
凰荚,即優(yōu)先使用內(nèi)存指針。通過提高內(nèi)存指針的使用褒脯,來降低堆區(qū)內(nèi)存的初始化便瑟。降低內(nèi)存消耗。在需要修改值的時候番川,會先檢測引用計數(shù)檢測到涂,如果有大于1的引用計數(shù)脊框,則開辟新內(nèi)存,創(chuàng)建新的實例践啄。在對內(nèi)容進行變更的時候浇雹,會開啟一塊新的內(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 = ... }}
這樣實現(xiàn)的目的:通過多份指針去引用同一份地址的成本遠遠低于開辟多份堆內(nèi)存伐谈。以下對比圖:
Protocol Type多態(tài)總結(jié)
支持Protocol Type的動態(tài)多態(tài)(Dynamic Polymorphism)行為烂完。
通過使用Witness Table和Existential Container來實現(xiàn)。
對于大數(shù)的拷貝可以通過Indirect Storage間接存儲來進行優(yōu)化诵棵。
說到動態(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
來實現(xiàn)嘶窄,進行進一步優(yōu)化。
泛型
我們接下來會討論泛型屬性的存儲方式和泛型方法是如何分派的距贷。泛型和Protocol Type
的區(qū)別在于:
泛型支持的是靜態(tài)多態(tài)柄冲。
每個調(diào)用上下文只有一種類型。
查看下面的示例忠蝗,foo
和bar
方法是同一種類型羊初。在調(diào)用鏈中會通過類型降級進行類型取代。
對于以下示例:
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í)行時什湘,Swift將泛型T綁定為調(diào)用方使用的具體類型,這里為Point bar(local) -->bar<T = Point>(local) //在調(diào)用內(nèi)部bar方法時晦攒,會使用foo已經(jīng)綁定的變量類型Point闽撤,可以看到,泛型T在這里已經(jīng)被降級脯颜,通過類型Point進行取代
泛型方法調(diào)用的具體實現(xiàn)為:
同一種類型的任何實例哟旗,都共享同樣的實現(xiàn),即使用同一個Protocol Witness Table栋操。
使用Protocol/Value Witness Table闸餐。
每個調(diào)用上下文只有一種類型:這里沒有使用
Existential Container
, 而是將Protocol/Value Witness Table
作為調(diào)用方的額外參數(shù)進行傳遞矾芙。變量初始化和方法調(diào)用舍沙,都使用傳入的
VWT
和PWT
來執(zhí)行。
看到這里剔宪,我們并不覺得泛型比Protocol Type
有什么更快的特性拂铡,泛型如何更快呢?靜態(tài)多態(tài)前提下可以進行進一步的優(yōu)化壹无,稱為特定泛型優(yōu)化。
泛型特化
靜態(tài)多態(tài):在調(diào)用站中只有一種類型
Swift使用只有一種類型的特點感帅,來進行類型降級取代斗锭。類型降級后,產(chǎn)生特定類型的方法
為泛型的每個類型創(chuàng)造對應的方法
這時候你可能會問失球,那每一種類型都產(chǎn)生一個新的方法岖是,代碼空間豈不爆炸?靜態(tài)多態(tài)下進行特定優(yōu)化
specialization
因為是靜態(tài)多態(tài)。所以可以進行很強大的優(yōu)化实苞,比如進行內(nèi)聯(lián)實現(xiàn)豺撑,并且通過獲取上下文來進行更進一步的優(yōu)化。從而降低方法數(shù)量硬梁。優(yōu)化后可以更精確和具體前硫。
例如:
func min<T:Comparable>(x:T, y:T) -> T { return y < x ? y : x}
從普通的泛型展開如下,因為要支持所有類型的min
方法荧止,所以需要對泛型類型進行計算屹电,包括初始化地址、內(nèi)存分配跃巡、生命周期管理等危号。除了對value的操作,還要對方法進行操作素邪。這是一個非常的的工程外莲。
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兔朦,比如Int偷线,編譯器可以通過泛型特化,進行類型取代(Type Substitute)沽甥,優(yōu)化為:
func min<Int>(x:Int, y:Int) -> Int { return y < x ? y :x}
泛型特化specilization
是何時發(fā)生的?
在使用特定優(yōu)化時声邦,調(diào)用方需要進行類型推斷,這里需要知曉類型的上下文摆舟,例如類型的定義和內(nèi)部方法實現(xiàn)亥曹。如果調(diào)用方和類型是單獨編譯的,就無法在調(diào)用方推斷類型的內(nèi)部實行恨诱,就無法使用特定優(yōu)化媳瞪,保證這些代碼一起進行編譯,這里就用到了whole module optimization
照宝。而whole module optimization
是對于調(diào)用方和被調(diào)用方的方法在不同文件時蛇受,對其進行泛型特化優(yōu)化的前提。
泛型進一步優(yōu)化
特定泛型的進一步優(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())
在用到多種泛型龙巨,且確定泛型類型不會在運行時修改時笼呆,就可以對成對泛型的使用進行進一步優(yōu)化。
優(yōu)化的方式是將泛型的內(nèi)存分配由指針指定旨别,變?yōu)閮?nèi)存內(nèi)聯(lián)诗赌,不再有額外的堆初始化消耗。請注意秸弛,因為進行了存儲內(nèi)聯(lián)铭若,已經(jīng)確定了泛型特定類型的內(nèi)存分布,泛型的內(nèi)存內(nèi)聯(lián)不能存儲不同類型递览。所以再次強調(diào)此種優(yōu)化只適用于在運行時不會修改泛型類型叼屠,即不能同時支持一個方法中包含line
和point
兩種類型。
whole module optimization
whole module optimization
是用于Swift編譯器的優(yōu)化機制绞铃【涤辏可以通過-whole-module-optimization
(或 -wmo
)進行打開。在XCode 8之后默認打開儿捧。 Swift Package Manager
在release模式默認使用whole module optimization
荚坞。
module是多個文件集合。
編譯器在對源文件進行語法分析之后菲盾,會對其進行優(yōu)化颓影,生成機器碼并輸出目標文件,之后鏈接器聯(lián)合所有的目標文件生成共享庫或可執(zhí)行文件懒鉴。
whole module optimization
通過跨函數(shù)優(yōu)化诡挂,可以進行內(nèi)聯(lián)等優(yōu)化操作,對于泛型临谱,可以通過獲取類型的具體實現(xiàn)來進行推斷優(yōu)化璃俗,進行類型降級方法內(nèi)聯(lián),刪除多余方法等操作悉默。
全模塊優(yōu)化的優(yōu)勢
編譯器掌握所有方法的實現(xiàn)城豁,可以進行內(nèi)聯(lián)和泛型特化等優(yōu)化,通過計算所有方法的引用麦牺,移除多余的引用計數(shù)操作。
通過知曉所有的非公共方法鞭缭,如果這寫方法沒有被使用剖膳,就可以對其進行消除。
如何降低編譯時間
和全模塊優(yōu)化相反的是文件優(yōu)化岭辣,即對單個文件進行編譯吱晒。這樣的好處在于可以并行執(zhí)行,并且對于沒有修改的文件不會再次編譯沦童。缺點在于編譯器無法獲知全貌仑濒,無法進行深度優(yōu)化叹话,全模塊優(yōu)化如何避免沒修改的文件再次編譯。
編譯器內(nèi)部運行過程分為:語法分析墩瞳,類型檢查驼壶,SIL
優(yōu)化,LLVM
后端處理喉酌。
語法分析和類型檢查一般很快热凹,SIL
優(yōu)化執(zhí)行了重要的Swift特定優(yōu)化,例如泛型特化和方法內(nèi)聯(lián)等泪电,該過程大概占用真?zhèn)€編譯時間的三分之一般妙。LLVM
后端執(zhí)行占用了大部分的編譯時間,用于運行降級優(yōu)化和生成代碼相速。
進行全模塊優(yōu)化后碟渺,SIL
優(yōu)化會將模塊再次拆分為多個部分,LLVM
后端通過多線程對這些拆分模塊進行處理突诬,對于沒有修改的部分苫拍,不會進行再處理。這樣就避免了修改一小部分攒霹,整個大模塊進行LLVM
后端執(zhí)行怯疤,并且多線程并行操作也會縮短處理時間。
擴展:Swift的隱藏“Bug”
Swift因為方法分派機制問題催束,所以在設計和優(yōu)化后集峦,會產(chǎn)生和我們常規(guī)理解不太一致的結(jié)果,這當然不能算Bug抠刺。但是還是要單獨進行說明塔淤,避免在開發(fā)過程中,因為對機制的掌握不足速妖,造成預期和執(zhí)行出入導致的問題高蜂。
Message dispatch
我們通過上面說明結(jié)合Static dispatch VS Dynamic dispatch對方法分派方式有了了解。這里需要對Objective-C
的方法分派方式進行說明罕容。
熟悉OC的人都知道备恤,OC采用了運行時機制使用obj_msgSend
發(fā)送消息,runtime非常的靈活锦秒,我們不僅可以對方法調(diào)用采用swizzling
哈雏,對于對象也可以通過isa-swizzling
來擴展功能歌逢,應用場景有我們常用的hook和大家熟知的KVO
栖疑。
大家在使用Swift進行開發(fā)時都會問预麸,Swift是否可以使用OC的運行時和消息轉(zhuǎn)發(fā)機制呢?答案是可以。
Swift可以通過關鍵字dynamic
對方法進行標記沉噩,這樣就會告訴編譯器捺宗,此方法使用的是OC的運行時機制。
注意:我們常見的關鍵字
@ObjC
并不會改變Swift原有的方法分派機制川蒙,關鍵字@ObjC
的作用只是告訴編譯器蚜厉,該段代碼對于OC可見。
總結(jié)來說派歌,Swift通過dynamic
關鍵字的擴展后弯囊,一共包含三種方法分派方式:Static dispatch
,Table dispatch
和Message dispatch
胶果。下表為不同的數(shù)據(jù)結(jié)構(gòu)在不同情況下采取的分派方式:
如果在開發(fā)過程中匾嘱,錯誤的混合了這幾種分派方式,就可能出現(xiàn)Bug早抠,以下我們對這些Bug進行分析:
此情況是在子類的extension中重載父類方法時霎烙,出現(xiàn)和預期不同的行為。
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é)果和預期不同:
Base()悬垃。indirectProperty // “This is Base”Sub()。indirectProperty // expected "this is Sub"甘苍,but is “This is Base” <- Unexpected!
在Base.directProperty
前添加dynamic
關鍵字就可以獲得"this is Sub"的結(jié)果尝蠕。Swift在extension 文檔中說明,不能在extension中重載已經(jīng)存在的方法载庭。
“Extensions can add new functionality to a type, but they cannot override existing functionality.”
會出現(xiàn)警告:Cannot override a non-dynamic class declaration from an extension
看彼。
出現(xiàn)這個問題的原因是,NSObject的extension是使用的Message dispatch
囚聚,而Initial Declaration
使用的是Table dispath
(查看上圖 Swift Dispatch Method)靖榕。extension重載的方法添加在了Message dispatch
內(nèi),沒有修改虛函數(shù)表顽铸,虛函數(shù)表內(nèi)還是父類的方法茁计,故會執(zhí)行父類方法。想在extension重載方法谓松,需要標明dynamic
來使用Message dispatch
星压。
協(xié)議的擴展內(nèi)實現(xiàn)的方法,無法被遵守類的子類重載:
protocol Greetable { func sayHi()}extension Greetable { func sayHi() { print("Hello") }}func greetings(greeter:Greetable) { greeter.sayHi()}
現(xiàn)在定義一個遵守了協(xié)議的類Person
鬼譬。遵守協(xié)議類的子類LoudPerson
:
class Person:Greetable {}class LoudPerson:Person { func sayHi() { print("sub") }}
執(zhí)行下面代碼結(jié)果為:
var sub:LoudPerson = LoudPerson()sub.sayHi() //sub
不符合預期的代碼:
var sub:Person = LoudPerson()sub.sayHi() //HellO <-使用了protocol的默認實現(xiàn)
注意娜膘,在子類LoudPerson
中沒有出現(xiàn)override
關鍵字∨◆ぃ可以理解為LoudPerson
并沒有成功注冊Greetable
在Witness table
的方法劲绪。所以對于聲明為Person
實際為LoudPerson
的實例男窟,會在編譯器通過Person
去查找盆赤,Person
沒有實現(xiàn)協(xié)議方法贾富,則不產(chǎn)生Witness table
,sayHi
方法是直接調(diào)用的牺六。解決辦法是在base類內(nèi)實現(xiàn)協(xié)議方法颤枪,無需實現(xiàn)也要提供默認方法∈缂剩或者將基類標記為final
來避免繼承畏纲。
進一步通過示例去理解:
// 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() {}}
以上代碼會出現(xiàn)錯誤,提示Declarations in extensions can not be overridden yet
宅荤。
總結(jié)
影響程序的性能標準有三種:初始化方式屑迂, 引用指針和方法分派。
文中對比了兩種數(shù)據(jù)結(jié)構(gòu):
Struct
和Class
的在不同標準下的性能表現(xiàn)冯键。Swift相比OC和其它語言強化了結(jié)構(gòu)體的能力惹盼,所以在了解以上性能表現(xiàn)的前提下,通過利用結(jié)構(gòu)體可以有效提升性能惫确。在此基礎上手报,我們還介紹了功能強大的結(jié)構(gòu)體的類:
Protocol Type
和Generic
。并且介紹了它們?nèi)绾沃С侄鄳B(tài)以及通過使用有條件限制的泛型如何讓程序更快雕薪。
參考資料
作者簡介
亞男昧诱,美團點評iOS工程師。2017年加入美團點評所袁,負責美團管家開發(fā)盏档,研究編譯器原理。目前正積極推動Swift組件化建設燥爷。
歡迎加入美團iOS技術(shù)交流群蜈亩,跟作者零距離交流。進群方式:請加美美同學的微信(微信號:MTDPtech01)前翎,回復:iOS稚配,美美會自動拉你進群。
---------- END ----------
招聘信息
我們餐飲生態(tài)技術(shù)部是一個技術(shù)氛圍活躍港华,大牛聚集的地方道川。新到店緊握真正的大規(guī)模SaaS實戰(zhàn)機會,多租戶、數(shù)據(jù)冒萄、安全臊岸、開放平臺等全方位的挑戰(zhàn)。業(yè)務領域復雜技術(shù)挑戰(zhàn)多尊流,技術(shù)和業(yè)務能力迅速提升帅戒,最重要的是,加入我們崖技,你將實現(xiàn)真正通過代碼來改變行業(yè)的夢想逻住。我們歡迎各端人才加入,Java優(yōu)先迎献。感興趣的同學趕緊發(fā)送簡歷至 zhaoyanan02@meituan.com瞎访,我們期待你的到來。