2014年,苹果公司在WWDC上发布Swift这一新的编程语言。通过几年的发展,Swift已经成为iOS开发语言的“中流砥柱”,Swift提供了很是灵活的高级别特性,例如协议、闭包、泛型等,而且Swift还进一步开发了强大的SIL(Swift Intermediate Language)用于对编译器进行优化,使得Swift相比Objective-C运行更快性能更优,Swift内部如何实现性能的优化,咱们本文就进行一下解读,但愿能对你们有所启发和帮助。html
针对Swift性能提高这一问题,咱们能够从概念上拆分为两个部分:git
下面咱们将从这两个角度切入,对Swift性能优化进行分析。经过了解编译器对不一样数据结构处理的内部实现,来选择最合适的算法机制,并利用编译器的优化特性,编写高性能的程序。github
理解Swift的性能,首先要清楚Swift的数据结构,组件关系和编译运行方式。算法
数据结构编程
Swift的数据结构能够大致拆分为:Class
,Struct
,Enum
。swift
组件关系后端
组件关系能够分为:inheritance
,protocols
,generics
。数组
方法分派方式安全
方法分派方式能够分为Static dispatch
和Dynamic dispatch
。性能优化
要在开发中提升Swift性能,须要开发者去了解这几种数据结构和组件关系以及它们的内部实现,从而经过选择最合适的抽象机制来提高性能。
首先咱们对于性能标准进行一个概念陈述,性能标准涵盖三个标准:
接下来,咱们会分别对这几个指标进行说明。
内存分配能够分为堆区栈区,在栈的内存分配速度要高于堆,结构体和类在堆栈分配是不一样的。
Stack
基本数据类型和结构体默认在栈区,栈区内存是连续的,经过出栈入栈进行分配和销毁,速度很快,高于堆区。
咱们经过一些例子进行说明:
//示例 1
// Allocation
// Struct
struct Point {
var x, y:Double
func draw() { … }
}
let point1 = Point(x:0, y:0) //进行point1初始化,开辟栈内存
var point2 = point1 //初始化point2,拷贝point1内容,开辟新内存
point2.x = 5 //对point2的操做不会影响point1
// use `point1`
// use `point2`
复制代码
以上结构体的内存是在栈区分配的,内部的变量也是内联在栈区。将point1
赋值给point2
实际操做是在栈区进行了一份拷贝,产生了新的内存消耗point2
,这使得point1
和point2
是彻底独立的两个实例,它们之间的操做互不影响。在使用point1
和point2
以后,会进行销毁。
Heap
高级的数据结构,好比类,分配在堆区。初始化时查找没有使用的内存块,销毁时再从内存块中清除。由于堆区可能存在多线程的操做问题,为了保证线程安全,须要进行加锁操做,所以也是一种性能消耗。
// Allocation
// Class
class Point {
var x, y:Double
func draw() { … }
}
let point1 = Point(x:0, y:0) //在堆区分配内存,栈区只是存储地址指针
let point2 = point1 //不产生新的实例,而是对point2增长对堆区内存引用的指针
point2.x = 5 //由于point1和point2是一个实例,因此point1的值也会被修改
// use `point1`
// use `point2`
复制代码
以上咱们初始化了一个Class
类型,在栈区分配一块内存,可是和结构体直接在栈内存储数值不一样,咱们只在栈区存储了对象的指针,指针指向的对象的内存是分配在堆区的。须要注意的是,为了管理对象内存,在堆区初始化时,除了分配属性内存(这里是Double类型的x,y),还会有额外的两个字段,分别是type
和refCount
,这个包含了type
,refCount
和实际属性的结构被称为blue box
。
内存分配总结
从初始化角度,Class
相比Struct
须要在堆区分配内存,进行内存管理,使用了指针,有更强大的特性,可是性能较低。
优化方式:
对于频繁操做(好比通讯软件的内容气泡展现),尽可能使用Struct
替代Class
,由于栈内存分配更快,更安全,操做更快。
Swift经过引用计数管理堆对象内存,当引用计数为0时,Swift确认没有对象再引用该内存,因此将内存释放。对于引用计数的管理是一个很是高频的间接操做,而且须要考虑线程安全,使得引用计数的操做须要较高的性能消耗。
对于基本数据类型的Struct
来讲,没有堆内存分配和引用计数的管理,性能更高更安全,可是对于复杂的结构体,如:
// Reference Counting
// Struct containing references
struct Label {
var text:String
var font:UIFont
func draw() { … }
}
let label1 = Label(text:"Hi", font:font) //栈区包含了存储在堆区的指针
let label2 = label1 //label2产生新的指针,和label1同样指向一样的string和font地址
// use `label1`
// use `label2`
复制代码
这里看到,包含了引用的结构体相比Class
,须要管理双倍的引用计数。每次将结构体做为参数传递给方法或者进行直接拷贝时,都会出现多份引用计数。下图能够比较直观的理解:
备注:包含引用类型的结构体出现Copy的处理方式
Class在拷贝时的处理方式:
引用计数总结
Class
在堆区分配内存,须要使用引用计数器进行内存管理。Struct
在栈区分配内存,无引用计数管理。Struct
经过指针管理在堆区的属性,对结构体的拷贝会建立新的栈内存,建立多份引用的指针,Class
只会有一份。优化方式
在使用结构体时:
咱们以前在Static dispatch VS Dynamic dispatch中提到过,可以在编译期肯定执行方法的方式叫作静态分派Static dispatch,没法在编译期肯定,只能在运行时去肯定执行方法的分派方式叫作动态分派Dynamic dispatch。
Static dispatch
更快,并且静态分派能够进行内联等进一步的优化,使得执行更快速,性能更高。
可是对于多态的状况,咱们不能在编译期肯定最终的类型,这里就用到了Dynamic dispatch
动态分派。动态分派的实现是,每种类型都会建立一张表,表内是一个包含了方法指针的数组。动态分派更灵活,可是由于有查表和跳转的操做,而且由于不少特色对于编译器来讲并不明确,因此至关于block了编译器的一些后期优化。因此速度慢于Static dispatch
。
下面看一段多态代码,以及分析实现方式:
//引用语义实现的多态
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总结
Class
默认使用Dynamic dispatch
,由于在编译期几乎每一个环节的信息都没法肯定,因此阻碍了编译器的优化,好比inline
和whole module inline
。
使用Static dispatch代替Dynamic dispatch提高性能
咱们知道Static dispatch
快于Dynamic dispatch
,如何在开发中去尽量使用Static dispatch
。
inheritance constraints
继承约束 咱们可使用final
关键字去修饰Class
,以今生成的Final class
,使用Static dispatch
。
access control
访问控制 private
关键字修饰,使得方法或属性只对当前类可见。编译器会对方法进行Static dispatch
。
编译器能够经过whole module optimization
检查继承关系,对某些没有标记final
的类经过计算,若是能在编译期肯定执行的方法,则使用Static dispatch
。 Struct
默认使用Static dispatch
。
Swift快于OC的一个关键是能够消解动态分派。
总结
Swift提供了更灵活的Struct
,用以在内存、引用计数、方法分派等角度去进行性能的优化,在正确的时机选择正确的数据结构,可使咱们的代码性能更快更安全。
延伸
你可能会问Struct
如何实现多态呢?答案是protocol oriented programming
。
以上分析了影响性能的几个标准,那么不一样的算法机制Class
,Protocol Types
和Generic code
,它们在这三方面的表现如何,Protocol Type
和Generic code
分别是怎么实现的呢?咱们带着这个问题看下去。
这里咱们会讨论Protocol Type如何存储和拷贝变量,以及方法分派是如何实现的。不经过继承或者引用语义的多态:
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协议的类型集合,多是point或者line
for d in drawables {
d.draw()
}
复制代码
以上经过Protocol Type
实现多态,几个类之间没有继承关系,故不能按照惯例借助V-Table
实现动态分派。
若是想了解Vtable和Witness table实现,能够进行点击查看,这里不作细节说明。 由于Point和Line的尺寸不一样,数组存储数据实现一致性存储,使用了Existential Container
。查找正确的执行方法则使用了 Protoloc Witness Table
。
Existential Container
是一种特殊的内存布局方式,用于管理遵照了相同协议的数据类型Protocol Type
,这些数据类型由于不共享同一继承关系(这是V-Table
实现的前提),而且内存空间尺寸不一样,使用Existential Container
进行管理,使其具备存储的一致性。
结构以下:
Protocol Type
的类型不一样,内存空间,初始化方法等都不相同,为了对Protocol Type
生命周期进行专项管理,用到了Value Witness Table
。Protocol Type
的方法分派。内存分布以下:
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
复制代码
为了实现Class
多态也就是引用语义多态,须要V-Table
来实现,可是V-Table
的前提是具备同一个父类即共享相同的继承关系,可是对于Protocol Type
来讲,并不具有此特征,故为了支持Struct
的多态,须要用到protocol oriented programming
机制,也就是借助Protocol Witness Table
来实现(细节能够点击Vtable和witness table实现,每一个结构体会创造PWT
表,内部包含指针,指向方法具体实现)。
用于管理任意值的初始化、拷贝、销毁。
Value Witness Table
的结构如上,是用于管理遵照了协议的Protocol Type
实例的初始化,拷贝,内存消减和销毁的。
Value Witness Table
在SIL
中还能够拆分为%relative_vwtable
和%absolute_vwtable
,咱们这里先不作展开。
Value Witness Table
和Protocol Witness Table
经过分工,去管理Protocol Type
实例的内存管理(初始化,拷贝,销毁)和方法调用。
咱们来借助具体的示例进行进一步了解:
// Protocol Types
// The Existential Container in action
func drawACopy(local :Drawable) {
local.draw()
}
let val :Drawable = Point()
drawACopy(val)
复制代码
在Swift编译器中,经过Existential Container
实现的伪代码以下:
// Protocol Types
// The Existential Container in action
func drawACopy(local :Drawable) {
local.draw()
}
let val :Drawable = Point()
drawACopy(val)
//existential container的伪代码结构
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,由于不一样类型在内存中是不一样的(small value内联在栈内,large value初始化在堆内,栈持有指针),因此方法的肯定也是和类型相关的,咱们知道,查找方法时是经过当前对象的地址,经过必定的位移去查找方法地址。
vwt.destructAndDeallocateBuffer(temp) //vwt进行生命周期管理,销毁内存
}
复制代码
咱们知道,Swift中Class
的实例和属性都存储在堆区,Struct
实例在栈区,若是包含指针属性则存储在堆区,Protocol Type
如何存储属性?Small Number经过Existential Container
内联实现,大数存在堆区。如何处理Copy呢?
在出现Copy状况时:
let aLine = Line(1.0, 1.0, 1.0, 3.0)
let pair = Pair(aLine, aLine)
let copy = pair
复制代码
会将新的Exsitential Container
的valueBuffer指向同一个value即建立指针引用,可是若是要改变值怎么办?咱们知道Struct
值的修改和Class
不一样,Copy是不该该影响原实例的值的。
这里用到了一个技术叫作Indirect Storage With Copy-On-Write
,即优先使用内存指针。经过提升内存指针的使用,来下降堆区内存的初始化。下降内存消耗。在须要修改值的时候,会先检测引用计数检测,若是有大于1的引用计数,则开辟新内存,建立新的实例。在对内容进行变动的时候,会开启一块新的内存,伪代码以下:
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) { //如何存在多份引用,则开启新内存,不然直接修改
storage = LineStorage(storage)
}
storage。start = ...
}
}
复制代码
这样实现的目的:经过多份指针去引用同一份地址的成本远远低于开辟多份堆内存。如下对比图:
支持Protocol Type
的动态多态(Dynamic Polymorphism
)行为。
经过使用Witness Table
和Existential Container
来实现。
对于大数的拷贝能够经过Indirect Storage
间接存储来进行优化。
说到动态多态Dynamic Polymorphism
,咱们就要问了,什么是静态多态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
来实现,进行进一步优化。
咱们接下来会讨论泛型属性的存储方式和泛型方法是如何分派的。泛型和Protocol Type
的区别在于:
foo
和bar
方法是同一种类型。对于如下示例:
func foo<T:Drawable>(local :T) {
bar(local)
}
func bar<T:Drawable>(local:T) { … }
let point = Point()
foo(point)
复制代码
分析方法foo
和bar
的调用过程:
//调用过程
foo(point)-->foo<T = Point>(point) //在方法执行时,Swift将泛型T绑定为调用方使用的具体类型,这里为Point
bar(local) -->bar<T = Point>(local) //在调用内部bar方法时,会使用foo已经绑定的变量类型Point,能够看到,泛型T在这里已经被降级,经过类型Point进行取代
复制代码
泛型方法调用的具体实现为:
Existential Container
, 而是将Protocol/Value Witness Table
做为调用方的额外参数进行传递。VWT
和PWT
来执行。看到这里,咱们并不以为泛型比Protocol Type
有什么更快的特性,泛型如何更快呢?静态多态前提下能够进行进一步的优化,称为特定泛型优化。
specialization
由于是静态多态。因此能够进行很强大的优化,好比进行内联实现,而且经过获取上下文来进行更进一步的优化。从而下降方法数量。优化后能够更精确和具体。例如:
func min<T:Comparable>(x:T, y:T) -> T {
return y < x ? y : x
}
复制代码
从普通的泛型展开以下,由于要支持全部类型的min
方法,因此须要对泛型类型进行计算,包括初始化地址、内存分配、生命周期管理等。除了对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
}
复制代码
在肯定入参类型时,好比Int,编译器能够经过泛型特化,进行类型取代(Type Substitute),优化为:
func min<Int>(x:Int, y:Int) -> Int {
return y < x ? y :x
}
复制代码
泛型特化specilization
是什么时候发生的?
在使用特定优化时,调用方须要进行类型推断,这里须要知晓类型的上下文,例如类型的定义和内部方法实现。若是调用方和类型是单独编译的,就没法在调用方推断类型的内部实行,就没法使用特定优化,保证这些代码一块儿进行编译,这里就用到了whole module optimization
。而whole module optimization
是对于调用方和被调用方的方法在不一样文件时,对其进行泛型特化优化的前提。
特定泛型的进一步优化:
// Pairs in our program using generic types
struct 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())
复制代码
在用到多种泛型,且肯定泛型类型不会在运行时修改时,就能够对成对泛型的使用进行进一步优化。
优化的方式是将泛型的内存分配由指针指定,变为内存内联,再也不有额外的堆初始化消耗。请注意,由于进行了存储内联,已经肯定了泛型特定类型的内存分布,泛型的内存内联不能存储不一样类型。因此再次强调此种优化只适用于在运行时不会修改泛型类型,即不能同时支持一个方法中包含line
和point
两种类型。
###whole module optimization whole module optimization
是用于Swift编译器的优化机制。能够经过-whole-module-optimization
(或 -wmo
)进行打开。在XCode 8以后默认打开。 Swift Package Manager
在release模式默认使用whole module optimization
。module是多个文件集合。
编译器在对源文件进行语法分析以后,会对其进行优化,生成机器码并输出目标文件,以后连接器联合全部的目标文件生成共享库或可执行文件。 whole module optimization
经过跨函数优化,能够进行内联等优化操做,对于泛型,能够经过获取类型的具体实现来进行推断优化,进行类型降级方法内联,删除多余方法等操做。
全模块优化的优点
如何下降编译时间 和全模块优化相反的是文件优化,即对单个文件进行编译。这样的好处在于能够并行执行,而且对于没有修改的文件不会再次编译。缺点在于编译器没法获知全貌,没法进行深度优化。下面咱们分析下全模块优化如何避免没修改的文件再次编译。
编译器内部运行过程分为:语法分析,类型检查,SIL
优化,LLVM
后端处理。
语法分析和类型检查通常很快,SIL
优化执行了重要的Swift特定优化,例如泛型特化和方法内联等,该过程大概占用整个编译时间的三分之一。LLVM
后端执行占用了大部分的编译时间,用于运行降级优化和生成代码。
进行全模块优化后,SIL
优化会将模块再次拆分为多个部分,LLVM
后端经过多线程对这些拆分模块进行处理,对于没有修改的部分,不会进行再处理。这样就避免了修改一小部分,整个大模块进行LLVM
后端的再次执行,除此外,使用多线程并行操做也会缩短处理时间。
Swift由于方法分派机制问题,因此在设计和优化后,会产生和咱们常规理解不太一致的结果,这固然不能算Bug。可是仍是要单独进行说明,避免在开发过程当中,由于对机制的掌握不足,形成预期和执行出入致使的问题。
Message dispatch
咱们经过上面说明结合Static dispatch VS Dynamic dispatch对方法分派方式有了了解。这里须要对Objective-C
的方法分派方式进行说明。
熟悉OC的人都知道,OC采用了运行时机制使用obj_msgSend
发送消息,runtime很是的灵活,咱们不只能够对方法调用采用swizzling
,对于对象也能够经过isa-swizzling
来扩展功能,应用场景有咱们经常使用的hook和你们熟知的KVO
。
你们在使用Swift进行开发时都会问,Swift是否可使用OC的运行时和消息转发机制呢?答案是能够。
Swift能够经过关键字dynamic
对方法进行标记,这样就会告诉编译器,此方法使用的是OC的运行时机制。
注意:咱们常见的关键字
@ObjC
并不会改变Swift原有的方法分派机制,关键字@ObjC
的做用只是告诉编译器,该段代码对于OC可见。
总结来讲,Swift经过dynamic
关键字的扩展后,一共包含三种方法分派方式:Static dispatch
,Table dispatch
和Message dispatch
。下表为不一样的数据结构在不一样状况下采起的分派方式:

若是在开发过程当中,错误的混合了这几种分派方式,就可能出现Bug,如下咱们对这些Bug进行分析:
SR-584 此状况是在子类的extension中重载父类方法时,出现和预期不一样的行为。
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" }
}
复制代码
执行如下代码,直接调用没有问题:
Base().directProperty // “This is Base”
Sub().directProperty // “This is Sub”
复制代码
间接调用结果和预期不一样:
Base()。indirectProperty // “This is Base”
Sub()。indirectProperty // expected "this is Sub",but is “This is Base” <- Unexpected!
复制代码
在Base.directProperty
前添加dynamic
关键字就能够得到"this is Sub"的结果。Swift在extension 文档中说明,不能在extension中重载已经存在的方法。
“Extensions can add new functionality to a type, but they cannot override existing functionality.”
会出现警告:Cannot override a non-dynamic class declaration from an extension
。
出现这个问题的缘由是,NSObject的extension是使用的Message dispatch
,而Initial Declaration
使用的是Table dispath
(查看上图 Swift Dispatch Method)。extension重载的方法添加在了Message dispatch
内,没有修改虚函数表,虚函数表内仍是父类的方法,故会执行父类方法。想在extension重载方法,须要标明dynamic
来使用Message dispatch
。
协议的扩展内实现的方法,没法被遵照类的子类重载:
protocol Greetable {
func sayHi()
}
extension Greetable {
func sayHi() {
print("Hello")
}
}
func greetings(greeter:Greetable) {
greeter.sayHi()
}
复制代码
如今定义一个遵照了协议的类Person
。遵照协议类的子类LoudPerson
:
class Person:Greetable {
}
class LoudPerson:Person {
func sayHi() {
print("sub")
}
}
复制代码
执行下面代码结果为:
var sub:LoudPerson = LoudPerson()
sub.sayHi() //sub
复制代码
不符合预期的代码:
var sub:Person = LoudPerson()
sub.sayHi() //HellO <-使用了protocol的默认实现
复制代码
注意,在子类LoudPerson
中没有出现override
关键字。能够理解为LoudPerson
并无成功注册Greetable
在Witness table
的方法。因此对于声明为Person
实际为LoudPerson
的实例,会在编译器经过Person
去查找,Person
没有实现协议方法,则不产生Witness table
,sayHi
方法是直接调用的。解决办法是在base类内实现协议方法,无需实现也要提供默认方法。或者将基类标记为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() // 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() {}
}
复制代码
以上代码会出现错误,提示Declarations in extensions can not be overridden yet
。
影响程序的性能标准有三种:初始化方式, 引用指针和方法分派。
文中对比了两种数据结构:Struct
和Class
的在不一样标准下的性能表现。Swift相比OC和其它语言强化告终构体的能力,因此在了解以上性能表现的前提下,经过利用结构体能够有效提高性能。
在此基础上,咱们还介绍了功能强大的结构体的类:Protocol Type
和Generic
。而且介绍了它们如何支持多态以及经过使用有条件限制的泛型如何让程序更快。
亚男,美团点评iOS工程师。2017年加入美团点评,负责专业版餐饮管家开发,研究编译器原理。目前正积极推进Swift组件化建设。
咱们餐饮生态技术部是一个技术氛围活跃,大牛汇集的地方。新到店紧握真正的大规模SaaS实战机会,多租户、数据、安全、开放平台等全方位的挑战。业务领域复杂技术挑战多,技术和业务能力迅速提高,最重要的是,加入咱们,你将实现真正经过代码来改变行业的梦想。咱们欢迎各端人才加入,Java优先。感兴趣的同窗赶忙发送简历至 zhaoyanan02@meituan.com,咱们期待你的到来。