当前位置: 首页 > 科技观察

HowtoWriteHigh-PerformanceSwiftCode

时间:2023-03-12 13:45:23 科技观察

文档可以帮助提高Swift程序的质量,使您的代码更不易出错且更具可读性。显式标记最终类和类协议是两个明显的例子。但是,文档中也有一些技巧是不规则的,扭曲的,比编译器或语言只能解决一些特殊的临时需求。文档中的许多建议来自各种权衡,例如:运行时、字节大小、代码可读性等。启用优化您应该做的第一件事是启用优化。Swift提供了三种不同的优化级别:-Onone:这意味着正常开发。它执行最少的优化并保存所有调试信息。-O:这适用于大多数生产代码。编译器执行积极的优化,可以彻底改变提交代码的类型和数量。调试信息将被省略,但仍然是有害的。-Ounchecked:这是一种特殊的优化模式,暗示特定的库或应用程序,以安全为代价。编译器将删除所有溢出检查以及一些隐式类型检查。这通常不被使用,因为它会导致内存安全问题和整数溢出。如果仔细检查代码,整数溢出和类型转换是安全的。在XcodeUI中,当前可以修改的优化级别如下:...整个组件优化默认情况下,Swift单独编译每个文件。这允许Xcode非常快速地并行编译多个文件。但是,单独编译每个文件会阻止某些编译器优化。Swift还可以把整个程序当做一个文件来编译,把程序当做一个编译单元来优化。可以使用命令行flag-whole-module-optimization激活此模式。以这种模式编译的程序很可能需要更长的编译时间,但运行速度可能更快。可以通过XCode构建设置中的“整体模块优化”激活此模式。减少动态调度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](虚函数表)间接调用的。如果使用动态关键字声明,Swift将通过调用Objective-C通知来发送调用。在这两种情况下,这都将比直接函数调用慢,因为它阻止了很多编译器优化[2]用于超出间接调用本身的程序开销。在性能关键代码中,人们通常希望限制这种动态行为。建议:当您知道声明不需要被覆盖时使用“final”。final关键字是对类、方法或属性声明的限制,这样的声明不能被覆盖。这意味着编译器可以调用直接函数调用而不是间接函数调用。比如下面的C.array1和D.array1会被[3]直接访问。相比之下,D.array2将通过vtable访问:finalclassC{//Nodeclarationsinclass'C'canbeoverridden.vararray1:[Int]funcdoSomething(){...}}classD{finalvararray1[Int]//'array1'cannotbeoverriddenbyacomputedproperty。vararray2:[Int]//'array2'*can*beoverriddenbyacomputedproperty.}funcusingC(c:C){c.array1[i]=...//CandirectlyaccessC.arraywithoutingthroughdynamicdispatch.c.doSomething()=...//CandirectlycallC.doSomethingwithoutgoingthroughdynamicdispatch.}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()//文件中没有声明这个类的子类。//编译器可以去掉virtualcallstodoSomething()//直接调用A的doSomething方法。}funcusingF(f:F)->Int{returnfar.}Priv使用容器类型的泛型容器Array和Dictionary是Swift标准库提供的一个重要特性。本节介绍如何以高效的方式使用这些类型。建议:在数组中使用值类型在Swift中,类型可以分为两个不同的类别:值类型(结构、枚举、元组)和引用类型(类)。一个关键的区别是NSArray不能包含值类型。因此,在使用值类型时,优化器不需要处理对NSArray的支持,可以节省大部分对数组的消耗。此外,与引用类型相比,值类型仅在递归包含引用类型时才需要引用计数器。如果您使用没有引用类型的值类型,则可以避免额外的开销,从而释放数组内的流量。//Don'tseaclasshere.structPhonebookEntry{varname:Stringvarnumber:[Int]}vara:[PhonebookEntry]请记住在使用大值类型和使用引用类型之间做出良好的权衡。在某些情况下,复制和移动大值类型的成本大于移除桥接和保持/释放的成本。建议:当不需要NSArray桥接时,使用ContiguousArray来存储引用类型。如果你需要一个引用类型的数组,并且数组不需要桥接到NSArray,使用ContiguousArray而不是Array:classC{...}vara:ContiguousArray=[C(...),C(...),...,C(...)]建议:使用适当的变异而不是对象分配。Swift中的所有标准库容器都使用COW(写时复制)来执行复制而不是立即复制。在许多情况下,这允许编译器通过保存容器而不是深层副本来保存不必要的副本。如果容器的引用计数大于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)uncheckedoperationSwiftby在执行普通计算时检查溢出方法解决整数溢出错误。这些检查在确定不会发生内存安全问题的高效代码中是不合适的。建议:当您确定不会发生溢出时,请使用未经检查的整数计算。在性能关键代码中,如果您知道代码是安全的,则可以忽略溢出检查。a:[Int]b:[Int]c:[Int]//前提条件:foralla[i],b[i]:a[i]+b[i]doesnotoverflow!foriin0...n{c[i]=a[i]&+b[i]}泛型Swift通过使用泛型类型提供了一个非常强大的抽象机制。Swift编译器发出一个具体的代码块,在任何T上执行MySwiftFunc。生成的代码需要一个函数指针表和一个包含T的框作为附加参数。MySwiftFunc和MySwiftFunc之间的不同行为通过传递不同的函数指针表和框提供的抽象大小来说明。一个通用示例:classMySwiftFunc{...}MySwiftFuncX//WillemitcodethatworkswithInt...MySwiftFuncY//...aswellasString。当启用优化器时,Swift编译器会寻找这段被调用的代码,并尝试确认调用中使用的具体类型(例如:非泛型类型)。如果泛型函数的定义对优化器可见并且知道具体类型,Swift编译器将生成一个具有特殊类型的特殊泛型函数。那么调用这个特殊函数的过程就可以避免关联泛型的消耗。一些通用示例:classMyStack{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{returnsself+1}}classGame:Playable{vart:Tinit(_v:T){t=v}funcplay(){for_in0...100_000_000{t=t.ping()}}}funcplay_a_game(game:Playable){//此检查允许优化器专门化//通用调用'play'ifletz=gameas?Game{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的数据是值类型,但是当一个数组作为参数传递时,由于它的copy-on-write特性,不会每次都复制。在我们的树示例中,我们通过将树的内容包装到数组中来减少复制成本。这个简单的变化对我们的树数据结构的性能产生了巨大的影响,将数组作为参数传递的成本从O(n)变为O(1)。structtree:P{varnode:[P?]init(){node=[thing]}}然而,使用数组来实现COW机制有两个明显的缺点。第一个问题是数组公开了诸如append和count之类的方法,这些方法在值包装的上下文中没有效果,这些方法使引用类型的包装变得棘手。也许我们可以通过创建一个封装结构并隐藏这些未使用的API来解决这个问题,但是第二个问题无法解决。第二个问题是数组内部有保证程序安全的代码和与OC交互的代码。Swift需要检查给定的后续列表是否在数组的边界内,并且在保存值时需要检查是否需要扩展存储空间。这些运行时检查会减慢速度。另一种方法是实现一个使用COW机制的专用数据结构,而不是将数组封装为值。构建此类数据结构的示例如下所示:finalclassRef{varval:Tinit(_v:T){val=v}}structBox{varref:Refinit(_x:T){ref=Ref(x)}varvalue:T{get{returnref.val}set{if(!isUniquelyReferencedNonObjC(&ref)){ref=Ref(newValue)return}ref.val=newValue}}}typeBox可以替换前面的例子数组不安全代码Swift语言类使用引用计数进行内存管理。每次访问对象时,Swift编译器都会插入增加引用计数的代码。例如,考虑一个遍历使用类实现的链表的示例。遍历链表是通过将引用移动到链表的下一个节点来完成的:elem=elem.next。每次移动此引用时,Swift都会增加下一个对象的引用计数并减少前一个对象的引用计数。这个引用计数昂贵但是不可避免只要你使用一个swiftclassfinalclassNode{varnext:Node?vardata:Int...}建议:使用非托管引用来避免引用计数的负载在注重效率的代码中你可以选择使用非托管参考。Unmanaged结构允许开发人员关闭特定引用的引用计数varRef:Unmanaged=Unmanaged.passUnretained(Head)whileletNext=Ref.takeUnretainedValue().next{...Ref=Unmanaged.passUnretained(Next)}协议建议:将只有类实现的协议标记为类协议Swift可以指定协议只能由类实现。标记只能由类实现的协议的一个好处是,编译器可以基于此优化程序。例如,如果ARC内存管理系统知道它正在处理一个类对象,它可以很容易地保持(增加对象的引用计数)。如果编译器不知道这一点,它必须假设结构也实现了协议,然后它必须准备好持有或释放不同的数据结构,这可能非常昂贵。如果限制只能由类实现,则将协议标记为类协议以获得更好的性能包含类型方法地址的类型约束表。进行动态分发时,先从对象中查这张表,再查表中的方法[2]这是因为编译器不知道具体要调用的方法[3]比如直接加载一个字段一个类或者直接调用一个方法[4]解释什么是COW[5]在某些情况下,优化器可以通过内联和ARC优化技术去除retain和release,因为不会造成复制