原标题:编写高性能的Swift代码
下面这篇文档收集了一系列编写高性能 Swift 代码的要诀和技巧。文档的目标读者是编译器和标准库开发人员。
文档中的一些技巧可以帮助提升您的 Swift 程序质量,使您的代码不容易出错且可读性更好。显式地标记最终类和类协议是两个显而易见的例子。 然而文档中还有一些技巧是不符合规矩的,扭曲的,仅仅解决一些比编译器或语言的特殊的临时性需求。文档中的很多建议来自于多方面的权衡,例如:运行时、字节大小、代码可读性等等。
启用优化第一个应该做的事情就是启用优化。Swift 提供了三种不同的优化级别:
- -Onone: 这意味着正常的开发。它执行最小优化和保存所有调试信息。
- -O: 这意味着对于大多数生产代码。编译器执行积极地优化,可以大大改变提交代码的类型和数量。调试信息将被省略但还是会有损害的。
- -Ounchecked: 这是一个特殊的优化模式,它意味着特定的库或应用程序,这是以安全性来交换的。编译器将删除所有溢出检查以及一些隐式类型检查。这不是在通常情况下使用的,因为它可能会导致内存安全问题和整数溢出。如果你仔细审查你的代码,那么对整数溢出和类型转换来说是安全的。
在 Xcode UI 中,可以修改的当前优化级别如下:
…
默认情况下 Swift 单独编译每个文件。这使得 Xcode 可以非常快速的并行编译多个文件。然而,分开编译每个文件可以预防某些编译器优化。Swift 也可以犹如它是一个文件一样编译整个程序,犹如就好像它是一个单一的编译单元一样优化这个程序。这个模式可以使用命令行 flag-whole-module-optimization 来激活。在这种模式下编译的程序将最最有可能需要更长时间来编译,单可以运行得更快。
这个模式可以通过 XCode 构建设置中的“Whole Module Optimization”来激活。
降低动态调度Swift 在默认情况下是一个类似 Objective-C 的非常动态的语言。与 Objective-C 不同的是,Swift 给了程序员通过消除和减少这种特性来提供运行时性能的能力。本节提供几个可被用于这样的操作的语言结构的例子。
动态调度类使用动态调度的方法和默认的属性访问。因此在下面的代码片段中,a.aProperty、a.doSomething() 和 a.doSomethingElse() 都将通过动态调度来调用:
classA{ varaProperty:[Int] funcdoSomething(){...} dynamicdoSomethingElse(){...} } classB:A{ overridevaraProperty{ get{...} set{...} } overridefuncdoSomething(){...} } funcusingAnA(a:A){ a.doSomething() a.aProperty=... }
在 Swift 中,动态调度默认通过一个 vtable[1](虚函数表)间接调用。如果使用一个 dynamic 关键字来声明,Swift 将会通过调用 Objective-C 通知来发送呼叫代替。这两种情况中,这种情况会比直接的函数调用较慢,因为它防止了对间接呼叫本身之外程序开销的许多编译器优化[2]。在性能关键的代码中,人们常常会想限制这种动态行为。
建议:当你知道声明不需要被重写时使用“final”。final 关键字是一个类、一个方法、或一个属性声明中的一个限制,使得这样的声明不得被重写。这意味着编译器可以呼叫直接的函数调用代替间接调用。例如下面的 C.array1 和 D.array1 将会被直接[3]访问。与之相反,D.array2 将通过一个虚函数表访问:
finalclassC{ //Nodeclarationsinclass'C'canbeoverridden. vararray1:[Int] funcdoSomething(){...} } classD{ finalvararray1[Int]//'array1'cannotbeoverriddenbyacomputedproperty. vararray2:[Int]//'array2'*can*beoverriddenbyacomputedproperty. } funcusingC(c:C){ c.array1[i]=...//CandirectlyaccessC.arraywithoutgoingthroughdynamicdispatch. c.doSomething()=...//CandirectlycallC.doSomethingwithoutgoingthroughvirtualdispatch. } funcusingD(d:D){ d.array1[i]=...//CandirectlyaccessD.array1withoutgoingthroughdynamicdispatch. d.array2[i]=...//WillaccessD.array2throughdynamicdispatch. }建议:当声明的东西不需要被文件外部被访问到的时候,就用“private”
将 private 关键词用在一个声明上,会限制对其进行了声明的文件的可见性。这会让编辑器有能力甄别出所有其它潜在的覆盖声明。如此,由于没有了任何这样的声明,使得编译器可以自动地推断出 final 关键词,并据此去掉对方面的间接调用和属性的访问。例如在如下的e.doSomething() 和 f.myPrivateVar 中,就将可以被直接访问,假定在同一个文件中,E,F 并没有任何覆盖的声明:
privateclassE{ funcdoSomething(){...} } classF{ privatevarmyPrivateVar:Int } funcusingE(e:E){ e.doSomething()//Thereisnosubclassinthefilethatdeclaresthisclass. //ThecompilercanremovevirtualcallstodoSomething() //anddirectlycallA’sdoSomethingmethod. } funcusingF(f:F)->Int{ returnf.myPrivateVar }高效的使用容器类型
通用的容器 Array 和 Dictionary 是有 Swift 标准库提供的一个重要的功能特性。本节将介绍如何用一种高性能的方式使用这些类型。
建议:在数组中使用值类型在 Swift 中,类型可以分为不同的两类:值类型(结构体,枚举,元组)和引用类型(类)。一个关键的区分是 NSArray 不能含有值类型。因此当使用值类型时,优化器就不需要去处理对 NSArray 的支持,从而可以在数组上省去大部分消耗。
此外,相比引用类型,如果值类型递归地含有引用类型,那么值类型仅仅需要引用计数器。而如果使用没有引用类型的值类型,就可以避免额外的开销,从而释放数组内的流量。
//Don'tuseaclasshere. structPhonebookEntry{ varname:String varnumber:[Int] } vara:[PhonebookEntry]
记住要在使用大值类型和使用引用类型之间做好权衡。在某些情况下,拷贝和移动大值类型数据的消耗要大于移除桥接和持有/释放的消耗。
建议:当 NSArray 桥接不必要时,使用 ContiguousArray 存储引用类型。如果你需要一个引用类型的数组,而且数组不需要桥接到 NSArray 时,使用 ContiguousArray 替代 Array:
classC{...} vara:ContiguousArray<C>=[C(...),C(...),...,C(...)]建议:使用适当的改变而不是对象分配。
在 Swift 中所有的标准库容器都使用 COW(copy-on-write) 执行拷贝代替即时拷贝。在很多情况下,这可以让编译器通过持有容器而不是深度拷贝,从而省掉不必要的拷贝。如果容器的引用计数大于 1 并容器时被改变时,就会拷贝底层容器。例如:在下面这种情况:当 d 被分配给 c 时不拷贝,但是当 d 经历了结构性的改变追加 2,那么 d 将会被拷贝,然后 2 被追加到 b:
varc:[Int]=[...] vard=c//Nocopywilloccurhere. d.append(2)//Acopy*does*occurhere.
如果用户不小心时,有时 COW 会引起额外的拷贝。例如,在函数中,试图通过对象分配执行修改。在 Swift 中,所有的参数传递时都会被拷贝一份,例如,参数在调用点之前持有一份,然后在调用的函数结束时释放。也就是说,像下面这样的函数:
funcappend_one(a:[Int])->[Int]{ a.append(1) returna } vara=[1,2,3] a=append_one(a)
尽管由于分配,a 的版本没有任何改变 ,在 append_one后也没有使用 , 但 a 也许会被拷贝。这可以通过使用 inout 参数来避免这个问题:
funcappend_one_in_place(inouta:[Int]){ a.append(1) } vara=[1,2,3] append_one_in_place(&a)未检查操作
Swift 通过在执行普通计算时检查溢出的方法解决了整数溢出的 bug。这些检查在已确定没有内存安全问题会发生的高效的代码中,是不合适的。
建议:当你确切的知道不会发生溢出时使用未检查整型计算。
在对性能要求高的代码中,如果你知道你的代码是安全的,那么你可以忽略溢出检查。
a:[Int] b:[Int] c:[Int] //Precondition:foralla[i],b[i]:a[i]+b[i]doesnotoverflow! foriin0...n{ c[i]=a[i]&+b[i] }泛型
Swift 通过泛型类型的使用,提供了一个非常强大的抽象机制 。Swift 编译器发出一个可以对任何 T 执行 MySwiftFunc<T> 的具体的代码块。生成的代码需要一个函数指针表和一个包含 T 的盒子作为额外的参数。MySwiftFunc<Int>和 MySwiftFunc<String> 之间的不同的行为通过传递不同的函数指针表和通过盒子提供的抽象大小来说明。一个泛型的例子:
classMySwiftFunc<T>{...} MySwiftFunc<Int>X//WillemitcodethatworkswithInt... MySwiftFunc<String>Y//...aswellasString.
当优化器启用时,Swift 编译器寻找这段代码的调用,并试着确认在调用中具体使用的类型(例如:非泛型类型)。如果泛型函数的定义对优化器来说是可见的,并知道具体类型,Swift 编译器将生成一个有特殊类型的特殊泛型函数。那么调用这个特殊函数的这个过程就可以避免关联泛型的消耗。一些泛型的例子:
classMyStack<T>{ funcpush(element:T){...} funcpop()->T{...} } funcmyAlgorithm(a:[T],length:Int){...} //ThecompilercanspecializecodeofMyStack[Int] varstackOfInts:MyStack[Int] //Usestackofints. foriin...{ stack.push(...) stack.pop(...) } vararrayOfInts:[Int] //Thecompilercanemitaspecializedversionof'myAlgorithm'targetedfor //[Int]'types. myAlgorithm(arrayOfInts,arrayOfInts.length)
建议:将泛型的声明放在使用它的文件中
只有在泛型声明在当前模块可见的情况下优化器才能执行特殊化。这只有在使用泛型的代码和声明泛型的代码在同一个文件中才能发生。注意标准库是一个例外。在标准库中声明的泛型对所有模块可见并可以进行特殊化。
建议:允许编译器进行特殊化
只有当调用位置和被调函数位于同一个编译单元的时候编译器才能对泛型代码进行特殊化。我们可以使用一个技巧让编译器对被调函数进行优化,这个技巧就是在被调函数所在的编译单元中执行类型检查。执行类型检查的代码会重新分发这个调用到泛型函数—可是这一次它携带了类型信息。在下面的代码中,我们在函数 play_a_game 中插入了类型检查,使得代码的速度提高了几百倍。
//Framework.swift: protocolPingable{funcping()->Self} protocolPlayable{funcplay()} extensionInt:Pingable{ funcping()->Int{returnself+1} } classGame<T:Pingable>:Playable{ vart:T init(_v:T){t=v} funcplay(){ for_in0...100_000_000{t=t.ping()} } } funcplay_a_game(game:Playable){ //Thischeckallowstheoptimizertospecializethe //genericcall'play' ifletz=gameas?Game<Int>{ z.play() }else{ game.play() } } ///-------------->8 //Application.swift: play_a_game(Game(10))大的值对象的开销
在 swift 语言中,值类型保存它们数据独有的一份拷贝。使用值类型有很多优点,比如值类型具有独立的状态。当我们拷贝值类型时(相当于复制,初始化参数传递等操作),程序会创建值类型的一个拷贝。对于大的值类型,这种拷贝时很耗费时间的,可能会影响到程序的性能。
让我们看一下下面这段代码。这段代码使用值类型的节点定义了一个树,树的节点包含了协议类型的其他节点,计算机图形场景经常由可以使用值类型表示的实体以及形态变化,因此这个例子很有实践意义
protocolP{} structNode:P{ varleft,right:P? } structTree{ varnode:P? init(){...} }
当树进行拷贝时(参数传递,初始化或者赋值)整个树都需要被复制.这是一项花销很大的操作,需要很多的 malloc/free 调用以及以及大量的引用计数操作
然而,我们并不关系值是否被拷贝,只要在这些值还在内存中存在就可以。
对大的值类型使用 COW(copy-on-write,写时复制和数组有点类似)
减少复制大的值类型数据开销的办法时采用写时复制行为(当对象改变时才进行实际的复制工作)。最简单的实现写时复制的方案时使用已经存在的写时复制的数据结构,比如数组。Swift 的数据是值类型,但是当数组作为参数被传递时并不每次都进行复制,因为它具有写时复制的特性。
在我们的 Tree 的例子中我们通过将 tree 的内容包装成一个数组来减少复制的代价。这个简单的改变对我们 tree 数据结构的性能影响时巨大的,作为参数传递数组的代价从 O(n) 变为 O(1)。
structtree:P{ varnode:[P?] init(){ node=[thing] } }
但是使用数组实现 COW 机制有两个明显的不足,第一个问题是数组暴露的诸如 append 以及 count 之类的方法在值包装的上下文中没有任何作用,这些方法使得引用类型的封装变得棘手。也许我们可以通过创建一个封装的结构体并隐藏这些不用的 API 来解决这个问题,但是却无法解决第二个问题。第二个问题就是数组内部存在保证程序安全性的代码以及和 OC 交互的代码。Swift 要检查给出的下表是否搂在数组的边界内,当保存值的时候需要检查是否需要扩充存储空间。这些运行时检查会降低速度。
一个替代的方案是实现一个专门的使用 COW 机制的数据结构代替采用数组作为值的封装。构建这样一个数据结构的示例如下所示:
finalclassRef<T>{ varval:T init(_v:T){val=v} } structBox<T>{ varref:Ref<T> init(_x:T){ref=Ref(x)} varvalue:T{ get{returnref.val} set{ if(!isUniquelyReferencedNonObjC(&ref)){ ref=Ref(newValue) return } ref.val=newValue } } }
类型 Box 可以代替上个例子中的数组
不安全的代码
Swift 语言的类都是采用引用计数进行内存管理的。Swift 编译器会在每次对象被访问的时候插入增加引用计数的代码。例如,考虑一个遍历使用类实现的一个链表的例子。遍历链表是通过移动引用到链表的下一个节点来完成的:elem = elem.next,每次移动这个引用,Swift 都要增加 next 对象的引用计数并减少前一个对象的引用计数,这种引用计数代价昂贵但是只要使用 swift 类就无法避免
finalclassNode{ varnext:Node? vardata:Int ... }
建议:使用未托管的引用避免引用计数的负荷
在效率至上的代码中你可以选择使用未托管的引用。Unmanaged<T>结构体允许开发者对特别的引用关闭引用计数
varRef:Unmanaged<Node>=Unmanaged.passUnretained(Head) whileletNext=Ref.takeUnretainedValue().next{ ... Ref=Unmanaged.passUnretained(Next) }协议
建议:将只有类实现的协议标记为类协议
Swift 可以指定协议只能由类实现。标记协议只能由类实现的一个好处是编译器可以基于这一点对程序进行优化。例如,ARC 内存管理系统能够容易的持有(增加该对象的引用计数)如果它知道它正在处理一个类对象。如果编译器不知道这一点,它就必须假设结构体也可以实现协议,那么它就必须准备好持有或者释放不同的数据结构,而这代价将会十分昂贵。
如果限制只能由类实现某协议那么就标记该协议为类协议以获得更好的性能
protocolPingable:class{funcping()->Int}脚注
【1】虚拟方法表或者 vtable 是被一个实例引用的一种包含类型方法地址的类型约束表。进行动态分发时,首先从对象中查找这张表然后查找表中的方法
【2】这是因为编译器并不知道那个具体的方法要被调用
【3】例如,直接加载一个类的字段或者直接调用一个方法
【4】解释 COW 是什么
【5】在特定情况下优化器能够通过内联和 ARC 优化技术移除 retain,release 因为没有引起复制
相关:
印象中零碳建筑(Zero-Energy Home)还是实验性很强的房子,少有兼具美观和宜居性的住宅。这所三层住宅就是
说在前面到了第二课,难度有所上升,可能需要你花更多的力气去消化,请注意,这不是一顿快餐.. It deserves
谈起太极拳的特点,肯定要提起太极拳的整劲和寸劲,习武者必练这两个劲。太极拳的好处是强身健体
周鸿祎在360新员工入职培训上的讲话,适合我们所有就业的年轻人
这个是周鸿祎给入司360新员工的一段话,不但适合360,也适合所有的公司员工。特此分享。——在360新员工入职
“早上好。再过不到一小时,这里的战机将会与全球的其它同盟一起,发动人类历史上最大的空战……” 1996 年
陈列目的到底是什么?难道只是为了美......NO?陈列的目的是为了 “提升销售”?凡是不能提
做了N年导购,却依旧只是导购!什么样的导购更容易成为超级卖手?!具备什么样的销售技巧的人才能被
小编想:阳光大男孩们应该都比较喜欢运动吧?那么我们在选择穿衣搭配的时候,应该也是倾向于运动休
羽毛球这项运动是一个老少皆宜,男女都可以进行的运动。它不受场地
(2015年第1号) 根据福建省商务厅《关于印发福建省商贸企业安全生产标准化建设提升工程三年行动实施方案的