编写高性能的 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() 都将通过动态调度来调用:

class A {
var aProperty: [Int]
func doSomething() { ... }
dynamic doSomethingElse() { ... }
}

class B : A {
override var aProperty {
get { ... }
set { ... }
}

override func doSomething() { ... }
}

func usingAnA(a: A) {
a.doSomething()
a.aProperty = ...
}

在 Swift 中,动态调度默认通过一个 vtable[1](虚函数表)间接调用。如果使用一个 dynamic 关键字来声明,Swift 将会通过调用 Objective-C 通知来发送呼叫代替。这两种情况中,这种情况会比直接的函数调用较慢,因为它防止了对间接呼叫本身之外程序开销的许多编译器优化[2]。在性能关键的代码中,人们常常会想限制这种动态行为。

建议:当你知道声明不需要被重写时使用“final”

final 关键字是一个类、一个方法、或一个属性声明中的一个限制,使得这样的声明不得被重写。这意味着编译器可以呼叫直接的函数调用代替间接调用。例如下面的 C.array1 和 D.array1 将会被直接[3]访问。与之相反,D.array2 将通过一个虚函数表访问:

final class C {
// No declarations in class 'C' can be overridden.
var array1: [Int]
func doSomething() { ... }
}

class D {
final var array1 [Int] // 'array1' cannot be overridden by a computed property.
var array2: [Int] // 'array2' *can* be overridden by a computed property.
}

func usingC(c: C) {
c.array1[i] = ... // Can directly access C.array without going through dynamic dispatch.
c.doSomething() = ... // Can directly call C.doSomething without going through virtual dispatch.
}

func usingD(d: D) {
d.array1[i] = ... // Can directly access D.array1 without going through dynamic dispatch.
d.array2[i] = ... // Will access D.array2 through dynamic dispatch.
}

建议:当声明的东西不需要被文件外部被访问到的时候,就用“private”

将 private 关键词用在一个声明上,会限制对其进行了声明的文件的可见性。这会让编辑器有能力甄别出所有其它潜在的覆盖声明。如此,由于没有了任何这样的声明,使得编译器可以自动地推断出 final 关键词,并据此去掉对方面的间接调用和属性的访问。例如在如下的 e.doSomething() 和 f.myPrivateVar 中,就将可以被直接访问,假定在同一个文件中,E,F 并没有任何覆盖的声明:

private class E {
func doSomething() { ... }
}

class F {
private var myPrivateVar : Int
}

func usingE(e: E) {
e.doSomething() // There is no sub class in the file that declares this class.
// The compiler can remove virtual calls to doSomething()
// and directly call A’s doSomething method.
}

func usingF(f: F) -> Int {
return f.myPrivateVar
}

高效的使用容器类型

通用的容器 Array 和 Dictionary 是有 Swift 标准库提供的一个重要的功能特性。本节将介绍如何用一种高性能的方式使用这些类型。

建议:在数组中使用值类型

在 Swift 中,类型可以分为不同的两类:值类型(结构体,枚举,元组)和引用类型(类)。一个关键的区分是 NSArray 不能含有值类型。因此当使用值类型时,优化器就不需要去处理对 NSArray 的支持,从而可以在数组上省去大部分消耗。

此外,相比引用类型,如果值类型递归地含有引用类型,那么值类型仅仅需要引用计数器。而如果使用没有引用类型的值类型,就可以避免额外的开销,从而释放数组内的流量。

// Don't use a class here.
struct PhonebookEntry {
var name : String
var number : [Int]
}

var a : [PhonebookEntry]

记住要在使用大值类型和使用引用类型之间做好权衡。在某些情况下,拷贝和移动大值类型数据的消耗要大于移除桥接和持有/释放的消耗。

建议:当 NSArray 桥接不必要时,使用 ContiguousArray 存储引用类型。


如果你需要一个引用类型的数组,而且数组不需要桥接到 NSArray 时,使用 ContiguousArray 替代 Array:

class C { ... }
var a: ContiguousArray<C> = [C(...),C(...),...,C(...)]

建议:使用适当的改变而不是对象分配。

在 Swift 中所有的标准库容器都使用 COW(copy-on-write) 执行拷贝代替即时拷贝。在很多情况下,这可以让编译器通过持有容器而不是深度拷贝,从而省掉不必要的拷贝。如果容器的引用计数大于 1 并容器时被改变时,就会拷贝底层容器。例如:在下面这种情况:当 d 被分配给 c 时不拷贝,但是当 d 经历了结构性的改变追加 2,那么 d 将会被拷贝,然后 2 被追加到 b:

var c: [Int] = [ ... ]
var d = c // No copy will occur here.
d.append(2) // A copy *does* occur here.

如果用户不小心时,有时 COW 会引起额外的拷贝。例如,在函数中,试图通过对象分配执行修改。在 Swift 中,所有的参数传递时都会被拷贝一份,例如,参数在调用点之前持有一份,然后在调用的函数结束时释放。也就是说,像下面这样的函数:

func append_one(a: [Int]) -> [Int] {
a.append(1)
return a
}

var a = [1,2,3]
a = append_one(a)

尽管由于分配,a 的版本没有任何改变 ,在 append_one 后也没有使用 , 但 a 也许会被拷贝。这可以通过使用 inout 参数来避免这个问题:

func append_one_in_place(inout a: [Int]) {
a.append(1)
}

var a = [1,3]
append_one_in_place(&a)

未检查操作

Swift 通过在执行普通计算时检查溢出的方法解决了整数溢出的 bug。这些检查在已确定没有内存安全问题会发生的高效的代码中,是不合适的。

建议:当你确切的知道不会发生溢出时使用未检查整型计算。

在对性能要求高的代码中,如果你知道你的代码是安全的,那么你可以忽略溢出检查。

a : [Int]
b : [Int]
c : [Int]

// Precondition: for all a[i],b[i]: a[i] + b[i] does not overflow!
for i in 0 ... n {
c[i] = a[i] &+ b[i]
}

泛型

Swift 通过泛型类型的使用,提供了一个非常强大的抽象机制 。Swift 编译器发出一个可以对任何 T 执行 MySwiftFunc<T> 的具体的代码块。生成的代码需要一个函数指针表和一个包含 T 的盒子作为额外的参数。MySwiftFunc<Int> 和 MySwiftFunc<String> 之间的不同的行为通过传递不同的函数指针表和通过盒子提供的抽象大小来说明。一个泛型的例子:

class MySwiftFunc<T> { ... }

MySwiftFunc<Int> X // Will emit code that works with Int...
MySwiftFunc<String> Y // ... as well as String.

当优化器启用时,Swift 编译器寻找这段代码的调用,并试着确认在调用中具体使用的类型(例如:非泛型类型)。如果泛型函数的定义对优化器来说是可见的,并知道具体类型,Swift 编译器将生成一个有特殊类型的特殊泛型函数。那么调用这个特殊函数的这个过程就可以避免关联泛型的消耗。一些泛型的例子:

class MyStack<T> {
func push(element: T) { ... }
func pop() -> T { ... }

}

func myAlgorithm(a: [T],length: Int) { ... }

// The compiler can specialize code of MyStack[Int]
var stackOfInts: MyStack[Int]
// Use stack of ints.
for i in ... {
stack.push(...)
stack.pop(...)
}

var arrayOfInts: [Int]
// The compiler can emit a specialized version of 'myAlgorithm' targeted for
// [Int]' types.
myAlgorithm(arrayOfInts,arrayOfInts.length)

建议:将泛型的声明放在使用它的文件中

只有在泛型声明在当前模块可见的情况下优化器才能执行特殊化。这只有在使用泛型的代码和声明泛型的代码在同一个文件中才能发生。注意标准库是一个例外。在标准库中声明的泛型对所有模块可见并可以进行特殊化。

建议:允许编译器进行特殊化

只有当调用位置和被调函数位于同一个编译单元的时候编译器才能对泛型代码进行特殊化。我们可以使用一个技巧让编译器对被调函数进行优化,这个技巧就是在被调函数所在的编译单元中执行类型检查。执行类型检查的代码会重新分发这个调用到泛型函数---可是这一次它携带了类型信息。在下面的代码中,我们在函数 play_a_game 中插入了类型检查,使得代码的速度提高了几百倍。

//Framework.swift:

protocol Pingable { func ping() -> Self }
protocol Playable { func play() }

extension Int : Pingable {
func ping() -> Int { return self + 1 }
}

class Game<T : Pingable> : Playable {
var t : T

init (_ v : T) {t = v}

func play() {
for _ in 0...100_000_000 { t = t.ping() }
}
}

func play_a_game(game : Playable ) {
// This check allows the optimizer to specialize the
// generic call 'play'
if let z = game as? Game<Int> {
z.play()
} else {
game.play()
}
}

/// -------------- >8

// Application.swift:

play_a_game(Game(10))

大的值对象的开销

在 swift 语言中,值类型保存它们数据独有的一份拷贝。使用值类型有很多优点,比如值类型具有独立的状态。当我们拷贝值类型时(相当于复制,初始化参数传递等操作),程序会创建值类型的一个拷贝。对于大的值类型,这种拷贝时很耗费时间的,可能会影响到程序的性能。

让我们看一下下面这段代码。这段代码使用值类型的节点定义了一个树,树的节点包含了协议类型的其他节点,计算机图形场景经常由可以使用值类型表示的实体以及形态变化,因此这个例子很有实践意义

protocol P {}
struct Node : P {
var left,right : P?
}

struct Tree {
var node : P?
init() { ... }
}

当树进行拷贝时(参数传递,初始化或者赋值)整个树都需要被复制.这是一项花销很大的操作,需要很多的 malloc/free 调用以及以及大量的引用计数操作

然而,我们并不关系值是否被拷贝,只要在这些值还在内存中存在就可以。

对大的值类型使用 COW(copy-on-write,写时复制和数组有点类似)

减少复制大的值类型数据开销的办法时采用写时复制行为(当对象改变时才进行实际的复制工作)。最简单的实现写时复制的方案时使用已经存在的写时复制的数据结构,比如数组。Swift 的数据是值类型,但是当数组作为参数被传递时并不每次都进行复制,因为它具有写时复制的特性。

在我们的 Tree 的例子中我们通过将 tree 的内容包装成一个数组来减少复制的代价。这个简单的改变对我们 tree 数据结构的性能影响时巨大的,作为参数传递数组的代价从 O(n) 变为 O(1)。

struct tree : P {
var node : [P?]
init() {
node = [ thing ]
}
}

但是使用数组实现 COW 机制有两个明显的不足,第一个问题是数组暴露的诸如 append 以及 count 之类的方法在值包装的上下文中没有任何作用,这些方法使得引用类型的封装变得棘手。也许我们可以通过创建一个封装的结构体并隐藏这些不用的 API 来解决这个问题,但是却无法解决第二个问题。第二个问题就是数组内部存在保证程序安全性的代码以及和 OC 交互的代码。Swift 要检查给出的下表是否搂在数组的边界内,当保存值的时候需要检查是否需要扩充存储空间。这些运行时检查会降低速度。

一个替代的方案是实现一个专门的使用 COW 机制的数据结构代替采用数组作为值的封装。构建这样一个数据结构的示例如下所示:

final class Ref<T> {
var val : T
init(_ v : T) {val = v}
}

struct Box<T> {
var ref : Ref<T>
init(_ x : T) { ref = Ref(x) }

var value: T {
get { return ref.val }
set {
if (!isUniquelyReferencedNonObjC(&ref)) {
ref = Ref(newValue)
return
}
ref.val = newValue
}
}
}

类型 Box 可以代替上个例子中的数组

不安全的代码

Swift 语言的类都是采用引用计数进行内存管理的。Swift 编译器会在每次对象被访问的时候插入增加引用计数的代码。例如,考虑一个遍历使用类实现的一个链表的例子。遍历链表是通过移动引用到链表的下一个节点来完成的:elem = elem.next,每次移动这个引用,Swift 都要增加 next 对象的引用计数并减少前一个对象的引用计数,这种引用计数代价昂贵但是只要使用 swift 类就无法避免

final class Node {
var next: Node?
var data: Int
...
}

建议:使用未托管的引用避免引用计数的负荷

在效率至上的代码中你可以选择使用未托管的引用。Unmanaged<T>结构体允许开发者对特别的引用关闭引用计数

var Ref : Unmanaged<Node> = Unmanaged.passUnretained(Head)

while let Next = Ref.takeUnretainedValue().next {
...
Ref = Unmanaged.passUnretained(Next)
}

协议

建议:将只有类实现的协议标记为类协议


Swift 可以指定协议只能由类实现。标记协议只能由类实现的一个好处是编译器可以基于这一点对程序进行优化。例如,ARC 内存管理系统能够容易的持有(增加该对象的引用计数)如果它知道它正在处理一个类对象。如果编译器不知道这一点,它就必须假设结构体也可以实现协议,那么它就必须准备好持有或者释放不同的数据结构,而这代价将会十分昂贵。

如果限制只能由类实现某协议那么就标记该协议为类协议以获得更好的性能

protocol Pingable : class { func ping() -> Int }


脚注

【1】虚拟方法表或者 vtable 是被一个实例引用的一种包含类型方法地址的类型约束表。进行动态分发时,首先从对象中查找这张表然后查找表中的方法
【2】这是因为编译器并不知道那个具体的方法要被调用
【3】例如,直接加载一个类的字段或者直接调用一个方法
【4】解释 COW 是什么
【5】在特定情况下优化器能够通过内联和 ARC 优化技术移除 retain,release 因为没有引起复制

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。