【轉(zhuǎn)】Swift編譯器深度剖析和如何開發(fā)高性能Swift程序

轉(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性能提升這一問題,我們可以從概念上拆分為兩個部分:

  1. 編譯器:Swift編譯器進行的性能優(yōu)化荧库,從階段分為編譯期和運行期堰塌,內(nèi)容分為時間優(yōu)化和空間優(yōu)化。

  2. 開發(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`
image

以上結(jié)構(gòu)體的內(nèi)存是在棧區(qū)分配的征字,內(nèi)部的變量也是內(nèi)聯(lián)在棧區(qū)都弹。將point1賦值給point2實際操作是在棧區(qū)進行了一份拷貝析命,產(chǎn)生了新的內(nèi)存消耗point2渐溶,這使得point1point2是完全獨立的兩個實例,它們之間的操作互不影響元莫。在使用point1point2之后氮昧,會進行銷毀框杜。

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`
image

以上我們初始化了一個Class類型,在棧區(qū)分配一塊內(nèi)存填渠,但是和結(jié)構(gòu)體直接在棧內(nèi)存儲數(shù)值不同弦聂,我們只在棧區(qū)存儲了對象的指針,指針指向的對象的內(nèi)存是分配在堆區(qū)的氛什。需要注意的是莺葫,為了管理對象內(nèi)存,在堆區(qū)初始化時枪眉,除了分配屬性內(nèi)存(這里是Double類型的x捺檬,y),還會有額外的兩個字段贸铜,分別是typerefCount堡纬,這個包含了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`
image

看到,包含了引用的結(jié)構(gòu)體相比Class绽左,需要管理雙倍的引用計數(shù)悼嫉。每次將結(jié)構(gòu)體作為參數(shù)傳遞給方法或者進行直接拷貝時,都會出現(xiàn)多份引用計數(shù)拼窥。下圖可以比較直觀的理解:

image

備注:包含引用類型的結(jié)構(gòu)體出現(xiàn)Copy的處理方式

Class在拷貝時的處理方式:

image

引用計數(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)體時:

  1. 通過使用精確類型,例如UUID替代String(UUID字節(jié)長度固定128字節(jié)骤视,而不是String任意長度)鞍爱,這樣就可以進行內(nèi)存內(nèi)聯(lián),在棧內(nèi)存儲UUID专酗,我們知道睹逃,棧內(nèi)存管理更快更安全,并且不需要引用計數(shù)祷肯。

  2. 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()}
image

Method Dispatch總結(jié)

Class默認使用Dynamic dispatch,因為在編譯期幾乎每個環(huán)節(jié)的信息都無法確定污茵,所以阻礙了編譯器的優(yōu)化樱报,比如inlinewhole 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 TypesGeneric code警绩,它們在這三方面的表現(xiàn)如何,Protocol TypeGeneric 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

image

Existential Container

Existential Container是一種特殊的內(nèi)存布局方式琉历,用于管理遵守了相同協(xié)議的數(shù)據(jù)類型Protocol Type坠七,這些數(shù)據(jù)類型因為不共享同一繼承關系(這是V-Table實現(xiàn)的前提)水醋,并且內(nèi)存空間尺寸不同,使用Existential Container進行管理彪置,使其具有存儲的一致性拄踪。

image

結(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))迹鹅。

image

Value Witness Table(VWT)

用于管理任意值的初始化卦洽、拷貝、銷毀斜棚。

image
  • Value Witness Table的結(jié)構(gòu)如上阀蒂,是用于管理遵守了協(xié)議的Protocol Type實例的初始化该窗,拷貝,內(nèi)存消減和銷毀的蚤霞。

  • Value Witness TableSIL中還可以拆分為%relative_vwtable%absolute_vwtable酗失,我們這里先不做展開。

  • Value Witness TableProtocol 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
image

會將新的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)存伐谈。以下對比圖:

image
image

Protocol Type多態(tài)總結(jié)

  1. 支持Protocol Type的動態(tài)多態(tài)(Dynamic Polymorphism)行為烂完。

  2. 通過使用Witness Table和Existential Container來實現(xiàn)。

  3. 對于大數(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)用上下文只有一種類型。
    查看下面的示例忠蝗,foobar方法是同一種類型羊初。

  • 在調(diào)用鏈中會通過類型降級進行類型取代。

對于以下示例:

func foo<T:Drawable>(local :T) { bar(local)}func bar<T:Drawable>(local:T) { … }let point = Point()foo(point)

分析方法foobar的調(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)用舍沙,都使用傳入的VWTPWT來執(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)化只適用于在運行時不會修改泛型類型叼屠,即不能同時支持一個方法中包含linepoint兩種類型。

whole module optimization

whole module optimization是用于Swift編譯器的優(yōu)化機制绞铃【涤辏可以通過-whole-module-optimization (或 -wmo)進行打開。在XCode 8之后默認打開儿捧。 Swift Package Manager在release模式默認使用whole module optimization荚坞。

module是多個文件集合。

image

編譯器在對源文件進行語法分析之后菲盾,會對其進行優(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),刪除多余方法等操作悉默。

image

全模塊優(yōu)化的優(yōu)勢

  • 編譯器掌握所有方法的實現(xiàn)城豁,可以進行內(nèi)聯(lián)泛型特化等優(yōu)化,通過計算所有方法的引用麦牺,移除多余的引用計數(shù)操作。

  • 通過知曉所有的非公共方法鞭缭,如果這寫方法沒有被使用剖膳,就可以對其進行消除。

如何降低編譯時間

和全模塊優(yōu)化相反的是文件優(yōu)化岭辣,即對單個文件進行編譯吱晒。這樣的好處在于可以并行執(zhí)行,并且對于沒有修改的文件不會再次編譯沦童。缺點在于編譯器無法獲知全貌仑濒,無法進行深度優(yōu)化叹话,全模塊優(yōu)化如何避免沒修改的文件再次編譯。

image

編譯器內(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 dispatchTable dispatchMessage dispatch胶果。下表為不同的數(shù)據(jù)結(jié)構(gòu)在不同情況下采取的分派方式:

image

如果在開發(fā)過程中匾嘱,錯誤的混合了這幾種分派方式,就可能出現(xiàn)Bug早抠,以下我們對這些Bug進行分析:

SR-584

此情況是在子類的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看彼。

image

出現(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星压。

SR-103

協(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并沒有成功注冊GreetableWitness table的方法劲绪。所以對于聲明為Person實際為LoudPerson的實例男窟,會在編譯器通過Person去查找盆赤,Person沒有實現(xiàn)協(xié)議方法贾富,則不產(chǎn)生Witness tablesayHi方法是直接調(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):StructClass的在不同標準下的性能表現(xiàn)冯键。Swift相比OC和其它語言強化了結(jié)構(gòu)體的能力惹盼,所以在了解以上性能表現(xiàn)的前提下,通過利用結(jié)構(gòu)體可以有效提升性能惫确。

  • 在此基礎上手报,我們還介紹了功能強大的結(jié)構(gòu)體的類:Protocol TypeGeneric。并且介紹了它們?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瞎访,我們期待你的到來。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末吁恍,一起剝皮案震驚了整個濱河市装诡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌践盼,老刑警劉巖鸦采,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異咕幻,居然都是意外死亡渔伯,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門肄程,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锣吼,“玉大人,你說我怎么就攤上這事蓝厌⌒” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵拓提,是天一觀的道長读恃。 經(jīng)常有香客問我,道長代态,這世上最難降的妖魔是什么寺惫? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮蹦疑,結(jié)果婚禮上西雀,老公的妹妹穿的比我還像新娘。我一直安慰自己歉摧,他們只是感情好艇肴,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布腔呜。 她就那樣靜靜地躺著,像睡著了一般再悼。 火紅的嫁衣襯著肌膚如雪育谬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天帮哈,我揣著相機與錄音,去河邊找鬼锰镀。 笑死娘侍,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的泳炉。 我是一名探鬼主播憾筏,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼花鹅!你這毒婦竟也來了氧腰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤刨肃,失蹤者是張志新(化名)和其女友劉穎古拴,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體真友,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡黄痪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了盔然。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桅打。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖愈案,靈堂內(nèi)的尸體忽然破棺而出挺尾,到底是詐尸還是另有隱情,我是刑警寧澤站绪,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布遭铺,位于F島的核電站,受9級特大地震影響恢准,放射性物質(zhì)發(fā)生泄漏掂僵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一顷歌、第九天 我趴在偏房一處隱蔽的房頂上張望锰蓬。 院中可真熱鬧,春花似錦眯漩、人聲如沸芹扭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舱卡。三九已至辅肾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間轮锥,已是汗流浹背矫钓。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留舍杜,地道東北人新娜。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像既绩,于是被迫代替她去往敵國和親概龄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容