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

轉(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è)部分:

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

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

以上結(jié)構(gòu)體的內(nèi)存是在棧區(qū)分配的,內(nèi)部的變量也是內(nèi)聯(lián)在棧區(qū)。將point1賦值給point2實(shí)際操作是在棧區(qū)進(jìn)行了一份拷貝,產(chǎn)生了新的內(nèi)存消耗point2,這使得point1point2是完全獨(dú)立的兩個(gè)實(shí)例,它們之間的操作互不影響。在使用point1point2之后,會(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`
image

以上我們初始化了一個(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è)字段,分別是typerefCount,這個(gè)包含了typerefCount和實(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`
image

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

image

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

Class在拷貝時(shí)的處理方式:

image

引用計(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í):

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

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

Method Dispatch總結(jié)

Class默認(rèn)使用Dynamic dispatch,因?yàn)樵诰幾g期幾乎每個(gè)環(huán)節(jié)的信息都無法確定,所以阻礙了編譯器的優(yōu)化,比如inlinewhole 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 dispatchStruct默認(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ī)制ClassProtocol TypesGeneric code,它們?cè)谶@三方面的表現(xiàn)如何,Protocol TypeGeneric 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

image

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ǔ)的一致性。

image

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

image

Value Witness Table(VWT)

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

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

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

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

會(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ì)比圖:

image
image

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

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

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

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

  • 在調(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)

分析方法foobar的調(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)用,都使用傳入的VWTPWT來執(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è)方法中包含linepoint兩種類型。

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è)文件集合。

image

編譯器在對(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),刪除多余方法等操作。

image

全模塊優(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)化如何避免沒修改的文件再次編譯。

image

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

image

如果在開發(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

image

出現(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è)GreetableWitness table的方法。所以對(duì)于聲明為Person實(shí)際為LoudPerson的實(shí)例,會(huì)在編譯器通過Person去查找,Person沒有實(shí)現(xiàn)協(xié)議方法,則不產(chǎn)生Witness tablesayHi方法是直接調(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):StructClass的在不同標(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 TypeGeneric。并且介紹了它們?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,我們期待你的到來。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評(píng)論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評(píng)論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評(píng)論 2 374

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

  • 簡介 在LLVM的官方文檔中對(duì)Swift的編譯器設(shè)計(jì)描述如下: Swift編程語言是在LLVM上構(gòu)建,并且使用L...
    sea_biscute閱讀 9,499評(píng)論 1 44
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,478評(píng)論 8 265
  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 16,000評(píng)論 3 119
  • 偶然的在印象筆記的推薦下開始使用簡書,發(fā)現(xiàn)原來還有很多有趣的人和事是我從沒有發(fā)現(xiàn)。 從以前就很喜歡寫出心中所想,然...
    亦步亦趨123閱讀 206評(píng)論 1 0