大多数浏览器和
Developer App 均支持流媒体播放。
-
探索 Swift 性能
探索 Swift 如何实现抽象概念与性能表现的完美平衡。了解需要考虑的性能相关因素,以及 Swift 优化器对这些因素有何影响。探索 Swift 的不同功能及其实现方式,以便进一步了解哪些方面的权衡会影响性能。
章节
- 0:00 - Introduction
- 1:24 - Agenda
- 1:46 - What is performance?
- 4:31 - Low-level principles
- 4:36 - Function calls
- 8:29 - Memory allocation
- 10:34 - Memory layout
- 13:57 - Value copying
- 20:54 - Putting it together
- 21:08 - Dynamically-sized types
- 24:33 - Async functions
- 28:11 - Closures
- 30:36 - Generics
- 34:00 - Wrap up
资源
相关视频
WWDC24
WWDC19
WWDC16
-
下载
我是 John McCall 今天我们将了解 Swift 性能 如果你需要经常使用某种编程语言 那么你应该具备对这种语言中 各种操作性能的敏锐感知 使用过 C 语言的程序员 通常具备这一特点 C 语言转译成机器码的过程 是非常字面的 这一点有利有弊 这类局部变量在栈上进行分配
堆分配仅在你进行调用时才会发生 编译器可能仍会将数据移入寄存器 优化内存并找出 各种提速的巧妙方式 但是 存在一套编译原则 这一点你可以放心
Swift 并没有那么简单 这在一定程度上与安全相关 如果你的代码存在错误 那么 C 语言转译过来的内容 将肆意篡改内存中的数据 相较于 C 语言 Swift 还提供许多抽象化工具 比如闭包、泛型等 这些工具的实现具有一定的复杂性 它们所隐含的开销 不像显式调用 malloc 那样明显可见 但这并不意味着你无法培养 同等敏锐的感知力 去洞悉你的代码实际上是如何运行的 这一点在你需要进行 性能优化工作时尤为重要 因此 在本次讲座中 我们将探索 Swift 的底层性能 首先 我们将介绍什么是性能 随后 我们将介绍在考虑底层性能时 你应该思考的准则 最后 我们将进一步了解 如何实现 Swift 中的主要功能 以及这些功能对性能有哪些影响
那么 到底什么是性能?
这是一个值得深度思考的问题 如果能够直接将程序 输入到某个工具中 随后这个工具就能输出一个反映 程序性能的数字 那该多好啊 这样 也许 Safari 浏览器 的性能得分能达到 9.2 但是 现实并非如此简单 性能是一个多维度的概念 并且与具体情境相关 通常 我们之所以关心性能 是因为遇到了某个宏观层面的问题 比如 守护程序耗电量太大 UI 在点按时反应异常缓慢 或者 App 一直被强制退出 对这些问题展开调查 通常需要自上而下地进行 你可以使用 Instruments 这类工具进行测量 它会指出需要深入分析的方面
大多数情况 你可以通过算法改进 来解决这些问题 而无需深入到代码的底层性能
但有时 你确实需要 深入探究底层性能 或许 你已经将调查范围 精确到执行追踪的某个环节 而在算法层面 你可能已经没有进一步改进的余地了 性能就是很慢 要进一步调查 还需要了解 代码的实际运行状况 而这需要采取更细致的 自下而上的方法 下面四个考虑因素 往往决定着底层性能 首先 我们进行的大量调用 并未得到有效优化 其次 由于数据的呈现方式不合理 导致我们耗费了大量时间或内存 第三 我们将太多时间 花在了内存分配上 第四 我们在值的拷贝和销毁上 花费了很多不必要的时间 Swift 中的大多数功能都能够影响这些开销中的一个或多个 我会逐一讲解各项功能 但在开始之前 我还要补充最后一点 Swift 有一个强大的优化器 有些性能问题你可能都意识不到 因为编译器能出色地消除这些问题 但优化是有限度的 你编写代码的方式会对 优化器的表现产生很大影响 因此 在谈及这个主题时 我还将介绍优化潜力 因为它是高性能编程的重要组成部分 如果你不喜欢依赖优化器 我有个建议 如果性能对于你的项目至关重要 那你就需要定期对性能进行监测 因此 当你在自上而下的 调查过程中识别热点时 设法找到测量它们的方法 然后在开发过程中自动执行这些测量 执行自动化测量后 你就能很好地定位性能下降问题 无论是你以某种方式误导了优化器的操作 还是因为你不小心添加了 一些二次算法 现在你要验证优化器 是否仍然按照你的要求工作
说到这里 让我们来深入了解 底层性能的四项原则 首先是函数调用
有四种开销与函数调用相关联 其中三项是我们执行的操作 首先 我们必须设置调用参数
我们还需要解析所调用函数的地址 而且我们必须为函数的 局部状态分配空间
第四项并非我们执行的操作 这一系列操作可能会抑制优化 既影响调用方 也影响调用的函数
我们来看 四种开销
首先是参数传递 这一开销分为两个层面 在最低层 在进行调用时 我们必须按照调用约定将参数放置在 在正确的位置上 在现代处理器上 这些开销 通常会被寄存器重命名所隐藏 因此 它们实际上不会造成太大的影响
不过从更高的层面来看 编译器可能需要添加值的副本 以匹配函数的所有权约定 在性能分析中 这通常显示为多余的保留与释放操作 这些操作发生在调用方或被调用方 稍后我还将继续介绍这方面内容 接下来的两项开销分别是 函数解析和优化影响 归根结底都涉及同一个问题 我们在编译时是否确切知道 我们将调用哪个函数? 如果答案是肯定的 我们就说这个调用使用了静态调度 否则 它就使用了动态调度 静态调度效率更高 它在处理器层面速度更快些 但更重要的是 如果编译器能够识别函数定义 则可在编译时实现大量优化 比如内联和泛型特化 但动态调度是使多态性 和其他强大的抽象工具 得以实现的关键
在 Swift 中 仅特定类型的调用使用 动态调度 而你可以通过查看调用的 声明来判断使用的是不是动态调度
在这个示例中 我对协议类型值 调用了 update 方法 调用的类型取决于方法的声明位置
如果是在协议主体中声明 属于协议要求 则调用使用的就是动态调度
但如果是在协议扩展中声明 则使用的是静态调度 无论是从语义角度还是性能方面 这都是非常重要的区别
函数调用的最后一项开销 是为局部状态分配内存 这个函数需要一些内存才能运行 它是一个普通的同步函数 所以它会在 C 栈上分配内存 在 C 栈上分配空间 可以通过对栈指针 执行减法操作来实现
如果你编译这段代码 你会得到 在函数开始和结束时 操作栈指针的汇编代码
当我们进入函数时 栈指针指向的是 C 栈 首先 我们在汇编代码中 从栈指针中减去一个特定的数值 可以看到这个数值是 208 字节 这会分配我们常说的 CallFrame 并为函数提供执行的空间 现在可以运行函数的主体了
在返回之前 我们将 208 字节 加回到栈指针 释放之前分配的内存
可以将 CallFrame 视为 具有类似 C 语言结构体的布局 理想情况下 函数的所有局部状态 在 CallFrame 中就会变成字段 我之所以说将内容 放在 CallFrame 中是理想情况 是因为编译器总是会 在函数开始时 生成那段减法操作的代码 只有这样 才能腾出空间 来保存返回地址等关键信息 减去一个较大的常量 并不会消耗更多的时间 因此 如果我们在函数中需要内存 在 CallFrame 中对它进行分配 可以说是接近零开销的做法
这与我们要了解的 下一个底层原则紧密相关 也就是内存分配
传统上 我们认为有三种内存 当然 对于电脑来说 这些 从根本上而言都来自同一个 RAM 池 但我们在程序中 会以不同的模式分配和使用内存 这一点对操作系统来说意义重大 进而对性能也有重要影响
全局内存会在载入程序时 进行分配并初始化 虽然不是零开销 但也十分接近了 全局内存的一大缺点是 它仅适用于具有固定内存容量的 特定模式 这一内存容量在整个程序运行期间都会持续存在 这与全局变量和静态成员变量的 特点非常吻合 但其他很多情况则不然
我们已经讨论过 CallFrame 作为栈分配的示例 与全局内存一样 栈内存的开销非常低 但它仅适用于某些特定模式 在这个示例中 内存必须限定范围 即当前函数中必须存在一个点 在这个点之后 我们可以确信这段内存不会再被使用 这非常符合典型的局部变量的特点
最后一种是堆内存 堆内存非常灵活 你可以在任意时间分配 和释放堆内存
这种灵活性 使得堆内存的分配和释放开销 比其他两种类型的内存 要高得多
堆内存不仅可用于存储 像类实例这样的明显对象 还可用于那些 因缺乏足够静态生命周期约束 而无法使用其他内存类型的功能特性
通常 在分配堆内存时 最终会出现共享所有权的问题 这意味着存在多个独立的引用 指向同一段内存 Swift 使用引用计数 来管理这些分配的生命周期
在 Swift 中 我们将引用计数递增称为保留 将引用计数递减称为释放
刚才我们谈到了分配内存 现在 我想介绍一下 Swift 如何使用内存来存储值 我们将这一过程称为内存布局
在大多数关于 Swift 的讨论中 我们所提到的值 往往是一个抽象的概念 而不涉及这些值在内存中的存储位置
例如 在初始化之后 我们可以说这个变量的值 是一个包含两个双精度浮点数的数组
当我们需要讨论 数据在内存中的表示形式时 有时人们仍会使用“值”这个词 在本次讨论中 为了避免产生混淆 我将使用技术性更高的词汇 “表示”来代替“值” 值的“表示”就是指数据值在内存中的布局 变量数组是内存中某一部分的名称 这部分内存中存储着 一个指向缓冲区对象的引用 当前 这个缓冲区对象已被初始化为 包含两个双精度浮点数值的 表示
我还将使用“内联表示”这个术语 它的意思是指不需要跟随任何指针 可以直接访问的表示部分 因此 我们变量数组的内联表示 是单个缓冲区引用 忽略这个缓冲区实际包含的内容 标准资料库中的 MemoryLayout 类型 仅测量内联表示的大小 因此 数组的内联表示只有 8 个字节 相当于一个 64 位指针的大小
好了 Swift 中的每个值 都存在于某种形式的上下文中 局部作用域包含作用域内部使用的所有值 其中包括局部变量、中间结果等 结构和类包含所有的存储属性 数组和字典通过它们的缓冲区 包含所有的元素 等等
Swift 中的每个值还拥有类型
值的类型决定了它在内存中的表示 包括值的内联表示 值的上下文决定了 用来存储内联表示的 内存源自哪里
那么 让我们来看看 示例中的具体情况 我们的数组是局部变量 因此 我们有一个数组值 这个数组值包含在局部作用域中
局部作用域会尽可能地将内联表示 放置在函数的 CallFrame 中 这里没有问题 在这个 CallFrame 中的某个位置 有空间可以存放 Double 数组的 内联表示
什么是 Double 数组的内联表示 数组是一种结构体 而结构体的内联表示 只是所有存储属性的内联表示 如果你查看标准资料库的源代码 这一点可能难以发现 不妨让我直接告诉你 归根结底 数组有单一的存储属性 而这个属性是类引用 类引用只是指向对象的指针
所以实际上 我们的 CallFrame 只存储这个指针
在 Swift 中 结构体、元组 和枚举全部使用内联存储 它们包含的所有内容 都在容器中进行内联布局 通常是按声明的顺序排列
类和 actor 使用非内联存储 它们包含的所有内容 都在对象中进行内联布局 容器只存储指向对象的指针 这一区别会对性能产生巨大的影响 为了解释清楚这些问题 我需要先介绍最后一个底层原则 这就是值拷贝 Swift 中有一个基本概念称为所有权 值的所有权意味着 负责管理这个值的表示
我们刚刚看到 数组的内联表示 是一个指向缓冲区对象的引用 这类引用通过使用引用计数进行管理 当我们说一个容器 拥有数组值的所有权时 那就意味着存在着一个不变性原则 即底层的数组缓冲区 作为将值存储到容器的一部分 已经被保留 然后 这个容器就有责任 最终通过释放操作 来平衡那个保留操作
如果没有其他影响 当容器消失时 这种情况肯定会发生 在这个示例中 容器是一个局部作用域 当变量超出作用域时 对象将会被释放
在 Swift 中 任何对值或变量的使用 都会以某种方式 与这个所有权系统交互 这是内存安全的关键一环 所有权交互方式主要有三种: 值消耗、值变异和值借用
值消耗意味着将值表示的所有权 从一个地方转移到另一个地方 理所当然地需要消耗值的最重要的操作 就是赋值到内存中
我们的示例中将演示这种情况 初始化变量 需要我们将初始值的所有权 转移到这个变量中
有时我们不使用副本也能完成这个操作 在这个示例中 变量的初始值是一个数组字面量 因此理所当然会生成一个新的独立值 Swift 可以将值的所有权 直接转移到变量中
如果我们用第一个变量的值 来初始化第二个变量 我们需要再次将值的所有权 转移到新的变量中 但现在 初始值表达式 并不能自然地生成一个新值 它只是引用一个现有变量 我们总不能直接从这个变量中窃取值 因为它可能还有更多的用途
为了获得一个独立的值 我们必须拷贝旧变量的当前值 由于值是一个数组 拷贝值就意味着保留它的缓冲区
现在 这是一项会频繁优化的功能 如果编译器能够确定 原始变量不会再被使用 它就应该能够在不使用副本的情况下转移值
你也可以使用 consume 运算符 来明确请求这一操作 如果你试图在变量 被明确消耗后继续使用它 Swift 将会报错并告诉你 那里已经不存在值了
值的第二种使用方式是变异 值变异意味着临时获取存储在一些可变变量中的 当前值的所有权 与值消耗的主要区别在于 变量仍可以在事后拥有值的所有权
当你像这样调用变异方法时 你实际上是将变量中当前值的所有权 转移给了这个方法 Swift 将阻止你在调用期间 同时以任何其他方式使用变量
当变异方法执行完成后 它会将新值的所有权 转移回原始变量 这维护了不变性原则 即变量拥有自身值的所有权
值的最后一种使用方法是借用 值借用意味着在借用期间 这个值不可以被其他人消费或变异 如果你只想读取某个值 自然就会想执行这个方法 你只关心在你使用这个值的期间 其他人无法改变或破坏它
传递参数是一个十分常见的场景 通常应当采取借用的方式进行 如果我将数组传递给 print 理想情况下 应该只传递信息 无需做任何额外的工作 然而 在某些情况下 Swift 需要对参数进行防御性拷贝而不是借用 为了借用一个值 Swift 必须证明没有其他同时尝试 变异或消耗值的操作 在这个简单的示例中 它应该能够可靠地完成这一任务 但在更复杂的情况下 Swift 有时很为难 例如 当存储被封装在类属性中时 Swift 可能很难证明 属性在同一时间没有被修改 因此可能需要添加一个防御性拷贝 Swift 正在积极改进这个方面 一方面改进优化器 另一方面增加了一些新功能 让你可以显式借用值来避免拷贝
拷贝值实际上意味着什么 这取决于值的内联表示 拷贝值意味着拷贝内联表示 以便获得拥有独立所有权的 新内联表示
这意味着拷贝类的值 表示拷贝引用的所有权 也就是保留所引用的对象 拷贝结构体的值意味着递归拷贝 结构体的所有存储属性
这表示在内联存储和 非内联存储之间进行选择 会涉及到一些实际的权衡取舍 内联存储可避免在堆上分配内存 这非常适合小型数据类型 对于更大的数据类型来说 如果你发现自己需要执行很多拷贝 拷贝的开销就会导致性能严重下降 并不存在一成不变的规则 可保证获得最佳性能
拷贝大型结构体的开销分为两部分 首先 当我们拷贝值类型时 拷贝的通常不仅仅是数位 这三个存储属性 都使用对象引用来表示 当我们拷贝包含这些属性的结构体时 这些引用都必须保留 因此 假设我们要拷贝的是类 拷贝时必须执行类对象的保留操作 但如果作为结构体进行拷贝 实际上仍然会对这些单独字段 执行三次保留操作
另外 这个值的每个副本 都需要为所有这些存储属性 分配独立的存储空间 因此 如果我们希望大量拷贝这个值 我们最终可能会使用更多内存 如果这种类型使用了非内联存储 每个副本都会引用同一个对象 因此内存会被重复使用 同样 没有一成不变的规则 但你也应该考虑一些因素
在 Swift 中 我们提倡使用值语义来编写类型 这意味着值拷贝会表现为完全独立于 原始值 结构体会有同样的行为 但始终使用内联存储 虽然类类型使用非内联存储 但它们天然就拥有引用语义 同时实现非内联存储和 值语义的一种方法是 将类封装在结构体中 然后使用写时复制 标准资料库在 Swift 的 所有基本数据结构中 运用了这一技术 这些数据结构包括数组、 字典和字符串
我们已经花了很多时间讨论 这四项基本原则 我们还了解了它们如何转换为 一些基本的 Swift 功能 比如结构体、类和函数 下面我们将这些内容综合起来 讨论 Swift 中的一些主要功能 我们先来了解大小可变的类型 C 语言中的结构体大小始终是固定的 但 Swift 中的类型 可以拥有运行时确定的大小 这主要包括以下两种情形
首先 SDK 中的很多值类型保留了 在未来操作系统更新中 添加和更改存储属性的权利 包括像 Foundation 的 URL 这样的类型 这意味着在编译时 必须将与这些值类型布局相关的一切 设为未知
其次 泛型类型的类型参数 通常可以替换为 具有任意可能表示的任何类型 因此 我们还是要 将类型布局设为未知
第二项规则存在一个例外情况 即当类型参数被约束为类的情况 在这个示例中 我们知道 类型参数必须具有类类型表示 也就是说 它始终是一个指针 这能够带来更高效的代码 即便在没有触发泛型替换的 情况下也是如此 前提是你能够接受这种约束
好了 当编译器无法静态确定 类型的表示时 Swift 是如何 处理内存布局和分配的呢? 这取决于存储值的容器类型 对于大多数容器 Swift 可以只在运行时进行布局 例如 这个 Connection 结构体 包含一个 URL 由于 URL 的布局无法静态获知 Connection 的布局也无法静态确定 不过这都没关系 因为这是包含 Connection 的集合 应该解决的问题 编译器知道 Connection 的静态布局 直到遇到第一个 可动态调整大小的属性 当这个程序首次需要 获取类型的布局时 剩余的布局将由 Swift 运行时动态填充
如果 URL 最终为 24 个字节 那么 Connection 在运行时的布局 就将与编译器静态获知这一信息时 的布局完全一致 编译器只能动态载入大小和偏移量 而无法使用常量
但有些容器必须具有固定大小 在这些情况下 编译器必须将值的内存分配 与容器的主要内存分配分开进行
举例来说 编译器只能请求 数量固定的全局内存 如果你声明一个 URL 类型的 全局变量 编译器将会创建一个 指针类型的全局变量 当你首次访问一个 具有惰性初始化属性的全局变量时 Swift 也将为这个变量 惰性分配堆空间
局部变量也会发生类似的情况 因为 CallFrame 也 必须具有固定大小
CallFrame 只是包含 指向 URL 的指针 当变量进入作用域时 函数就需要对变量进行动态分配 并在变量超出作用域时再将它释放
不过 由于局部变量具有作用域限制 这种分配仍然可以 在 C 栈上完成 当我们进入函数时 我们会照常分配 CallFrame
当变量进入作用域时 我们只需再对栈指针 进行一次减法操作 减去变量的大小
当变量超出作用域时 我们可以将栈指针重置为原来的值
到目前为止 我们只讨论了同步函数 那么异步函数又如何呢
异步函数的核心理念是 C 语言线程是一种宝贵的资源 如果使用 C 语言线程 仅仅是为了阻塞操作 那这并不是合理的资源利用方式 因此 异步函数 是通过两种特殊的方式实现的 首先 函数将局部状态保存在 独立于 C 栈的栈上 然后 函数实际上会在运行时 拆分为多个函数来执行
下面 我们来看一个异步函数示例
这个函数中存在一个 潜在的暂停点“await”
所有这些局部函数的用途 都跨越了这个暂停点 因此 它们无法被保存在 C 语言的栈中
我们刚才介绍了同步函数 通过从栈指针中执行减法操作 来分配 C 栈上的局部内存
异步函数在本质上 遵循相似的工作原理 只不过它们不会从 大型连续栈进行分配
相反 异步任务 会驻留一块或多块内存
当异步函数希望在 异步栈上分配内存时 就会向任务请求内存 栈会尝试从 当前 slab 中满足这一请求 如果可以 就更好了
任务会将 slab 中的这部分内存 标记为“已使用”并将它交给函数
但是 如果 slab 的大部分都 已被占用 则无法执行这一内存分配 那么 任务必须使用 malloc 来分配新的 slab
然后再执行内存分配
无论哪种情况 解除分配都会将内存交还给任务 并且内存会被标记为“未使用”
由于这个分配器仅由单个任务使用 并且采用栈式分配原则 它通常比 malloc 快得多 整体性能配置文件与同步函数类似 只是调用开销会稍高一些
为了真正实现运行 异步函数必须拆分为多个部分函数 这些函数跨越 潜在的函数暂停点之间的空隙 在这个示例中 因为函数中有一个 await 我们最终得到了两个部分函数
第一个部分函数从 原始函数的入口开始执行 如果数组为空 就会直接返回至异步调用方 否则 将拉出第一个任务并等待执行 另一个部分函数 则在 await 之后接续执行 首先 它将所等待任务的结果 添加到输出数组中 然后尝试继续执行循环 如果没有其他任务 就会返回至异步调用方 否则 函数将循环返回 并等待下一个任务
这里的关键点是 在 C 栈上 至多只有一个部分函数
我们输入一个部分函数 然后像普通 C 语言函数一样运行 直到出现下一个潜在的暂停点 如果部分函数需要某些 无需跨越暂停点的局部状态 函数可以将这些状态分配到它的 C CallFrame 中
这时 部分函数将以尾调用的方式 调用下一个部分函数 函数的 CallFrame 就会从 C 栈中消失 并分配下一个帧
然后 这个部分函数就会一直运行 直至达到潜在暂停点
如果一个任务确实需要暂停 只需要在 C 栈上正常返回 这通常会直接进入并发运行时环境 这样一来 线程可以立即被重复用于其他任务
到目前为止 在我的所有示例中 我一直展示的是函数声明 那么闭包是如何工作的 它对局部分配有何影响呢?
闭包总是作为函数类型的值用于传递 这个函数接收的参数 是一个非逃逸函数 Swift 中的函数值始终为 一对函数指针和上下文指针 因此 在 C 语言的术语里 这个函数签名看起来大致是这样的
在 Swift 中 对函数值的调用 实际上就是调用函数指针 并将上下文指针作为 隐式的附加参数进行传递
从封闭作用域捕获值的闭包表达式 必须将这些值打包进上下文中 具体如何工作取决于 需要产生的函数值的类型 在这个示例中 使用的函数是非逃逸函数 因此 我们知道这个函数值 在调用完成后将不再使用 这意味着 函数值不需要进行内存管理 我们可以使用 作用域分配来分配上下文
因此 上下文就是 一个包含被捕获值的简单结构体
上下文可以在栈上分配 分配的地址将被传递给 sumTwice 函数
在闭包函数中 我们知道成对上下文的类型 可以直接从中提取我们需要的数据 但对于逃逸闭包 情况则不同 我们无法确定 闭包仅会在调用期间被使用 因此 上下文对象必须在堆上分配 并且需要通过保留 和释放操作进行管理
本质上 上下文的行为 类似于 Swift 中匿名类的实例
现在 在 Swift 中 当你在闭包中引用局部变量时 你是通过引用的方式来捕获这个变量 这样就可以对变量执行更改 这些更改将在原始作用域中可见 反之亦然
如果变量仅被非逃逸闭包所捕获 这并不会改变变量的生命周期 因此 闭包可以通过 仅捕获指向变量分配的指针 来解决这个问题
但是 如果变量被逃逸闭包所捕获 只要闭包处于活动状态 变量的生命周期就可以延长
因此 变量也必须在堆上进行分配 并且闭包上下文 必须保留这个对象的引用
最后 我们来了解一下泛型
这个函数对它的数据模型 采用了泛型设计 我们已经介绍了这种类型的布局 在静态时是未知的 以及在不同的容器中 如何处理这个问题 我们还没有介绍过 协议约束的运作方式 具体来说 Swift 究竟如何执行 这个使用了某一协议要求的调用呢
Swift 协议在运行时是通过 一个函数指针表来表示的 这个表中的每个条目 对应着协议中的一个要求 在 C 语言中 这个表大致看起来像这样
每当我们有协议约束时 我们就会传递一个 指向对应表格的指针
在像这样的泛型函数中 类型表和见证表会变成 隐藏的额外参数 在运行时 这个签名中的每一部分 都与 Swift 原始签名中的 相应部分一一对应
当我们使用协议类型的值时 情况就有所不同 在这个示例中 我们让这个函数更加灵活 现在 数组中的每个元素 都可以是不同类型的数据模型 但这会影响运行效率
协议的内联表示 比如 AnyDataModel 在 C 语言中是这样的 我们为值提供了存储空间 并提供字段来记录值的类型 和我们已知的协议遵循信息
但必须是固定大小的类型 这种类型的表示无法更改大小 以便支持不同类型的数据模型 无论我们为值预留多大的存储空间 总会存在无法容纳的数据模型 我们该怎么做
Swift 使用了一个任意大小的 缓冲区 可容纳 3 个指针 如果这个缓冲区可以容纳 协议类型中所存储的值 Swift 将会把这个值 内联存储到这里 否则 就会为值分配堆空间 并且只在缓冲区中存储指针
所以 这些函数签名看起来非常相似 但它们实际上有着截然不同的特征 第一个函数接收数据模型同质数组 这些数据模型将高效地封装在数组中 类型信息将作为独立的顶层参数 一次性传递给函数
如果调用方知道 函数调用时所使用的类型 则函数也可以被特化 在这里 我们通过已知类型的数组调用函数 优化器可轻松内联这一调用 也可以为这个确切的参数类型 生成一个特化的函数版本 这就消除了 与泛型相关的任何抽象成本 使得更新调用直接到达 MyDataModel 遵循的实现中
第二个函数接收数据模型同质数组 这种方式更灵活 如果你有不同类型的数据模型 这种函数可能就是你需要的 但数组中的每个元素 如今都拥有了专属的动态类型 并且值在数组中不会密集排列
而实际优化这一点也比较困难 编译器必须完美地推断 数据是如何流入数组的 以及函数是如何利用数据的 这并不意味着性能一定会很差 但确实会减少编译器在优化代码方面 所能提供的帮助 在结束本次讲座前 我想给大家讲的就是这个问题 请不要有这样的想法: “John 让我们不要使用协议类型” 通过这次讲座 我希望传达的开销理念是 在某些情况下 无论什么开销 都是值得付出的 因为抽象化是强大而实用的工具 你应该充分利用它的优势 希望本次讲座中提供的信息 能帮助你建立起对 Swift 代码性能的敏锐感知 感谢观看
-
-
0:24 - An example C function, with self-evident allocation
int main(int argc, char **argv) { int count = argc - 1; int *arr = malloc(count * sizeof(int)); int i; for (i = 0; i < count; ++i) { arr[i] = atoi(argv[i + 1]); } free(arr); }
-
0:50 - An example Swift function, with a lot of implicit abstraction
func main(args: [String]) { let arr = args.map { Int($0) ?? 0 } }
-
4:39 - An example of a function call
URLSession.shared.data(for: request)
-
6:30 - A Swift function that calls a method on a value of protocol type
func updateAll(models: [any DataModel], from source: DataSource) { for model in models { model.update(from: source) } }
-
6:40 - A declaration of the method where it's a protocol requirement using dynamic dispatch
protocol DataModel { func update(from source: DataSource) }
-
6:50 - A declaration of the method where it's a protocol extension method using static dispatch
protocol DataModel { func update(from source: DataSource, quickly: Bool) } extension DataModel { func update(from source: DataSource) { self.update(from: source, quickly: true) } }
-
7:00 - The same function as before, which we're now talking about the local state within
func updateAll(models: [any DataModel], from source: DataSource) { for model in models { model.update(from: source) } }
-
7:18 - Partial assembly code for that function, showing instructions to adjust the stack pointer
_$s4main9updateAll6models4fromySayAA9DataModel_pG_AA0F6SourceCtF: sub sp, sp, #208 stp x29, x30, [sp, #192] … ldp x29, x30, [sp, #192] add sp, sp, #208 ret
-
7:59 - A C struct showing one possible layout of the function's call frame
// sizeof(CallFrame) == 208 struct CallFrame { Array<AnyDataModel> models; DataSource source; AnyDataModel model; ArrayIterator iterator; ... void *savedX29; void *savedX30; };
-
10:50 - A line of code containing a single variable initialization
var array = [ 1.0, 2.0 ]
-
11:44 - Using the MemoryLayout type to examine a type's inline representation
MemoryLayout.size(ofValue: array) == 8
-
12:48 - The variable initialization from before, now placed within a function
func makeArray() { var array = [ 1.0, 2.0 ] }
-
15:42 - Initializing a second variable with the contents of the first
func makeArray() { var array = [ 1.0, 2.0 ] var array2 = array }
-
16:27 - Taking the value of an existing variable with the consume operator
func makeArray() { var array = [ 1.0, 2.0 ] var array2 = consume array }
-
16:58 - A call to a mutating method
func makeArray() { var array = [ 1.0, 2.0 ] array.append(3.0) }
-
17:40 - Passing an argument that should be borrowable
func makeArray() { var array = [ 1.0, 2.0 ] print(array) }
-
18:10 - Passing an argument that will likely have to be defensively copied
func makeArray(object: MyClass) { object.array = [ 1.0, 2.0 ] print(object.array) }
-
19:27 - Part of a large struct type
struct Person { var name: String var birthday: Date var address: String var relationships: [Relationship] ... }
-
21:22 - A Connection struct that contains a property of the dynamically-sized URL type
struct Connection { var username: String var address: URL var options: [String: String] }
-
21:40 - A GenericConnection struct that contains a property of an unknown type parameter type
struct GenericConnection<T> { var username: String var address: T var options: [String: String] }
-
21:51 - The same GenericConnection struct, except with a class constraint on the type parameter
struct GenericConnection<T> where T: AnyObject { var username: String var address: T var options: [String: String] }
-
22:27 - The same Connection struct as before
struct Connection { var username: String var address: URL var options: [String: String] }
-
23:23 - A global variable of URL type
var address = URL(string: "...")
-
23:42 - A local variable of URL type
func workWithAddress() { var address = URL(string: "...") }
-
25:02 - An async function
func awaitAll(tasks: [Task<Int, Never>]) async -> [Int] { var results = [Int]() for task in tasks { results.append(await task.value) } return results }
-
28:21 - A function that takes an argument of function type
func sumTwice(f: () -> Int) -> Int { return f() + f() }
-
28:30 - A C function roughly corresponding to the Swift function
Int sumTwice(Int (*fFunction)(void *), void *fContext) { return fFunction(fContext) + fFunction(fContext); }
-
28:47 - A function call that passes a closure expression as a function argument
func sumTwice(f: () -> Int) -> Int { return f() + f() } func puzzle(n: Int) -> Int { return sumTwice { n + 1 } }
-
29:15 - C code roughly corresponding to the emission of the non-escaping closure
struct puzzle_context { Int n; }; Int puzzle(Int n) { struct puzzle_context context = { n }; return sumTwice(&puzzle_closure, &context); } Int puzzle_closure(void *_context) { struct puzzle_context *context = (struct puzzle_context *) _context; return _context->n + 1; }
-
29:34 - The function and its caller again, now taking an escaping function as its parameter
func sumTwice(f: @escaping () -> Int) -> Int { return f() + f() } func puzzle(n: Int) -> Int { return sumTwice { n + 1 } }
-
29:53 - A closure that captures a local variable by reference
func sumTwice(f: () -> Int) -> Int { return f() + f() } func puzzle(n: Int) -> Int { var addend = 0 return sumTwice { addend += 1 return n + addend } }
-
30:30 - Swift types roughly approximating how escaping variables and closures are handled
class Box<T> { let value: T } class puzzle_context { let n: Int let addend: Box<Int> }
-
30:40 - A generic function that calls a protocol requirement
protocol DataModel { func update(from source: DataSource) } func updateAll<Model: DataModel>(models: [Model], from source: DataSource) { for model in models { model.update(from: source) } }
-
31:03 - A C struct roughly approximating a protocol witness table
struct DataModelWitnessTable { ConformanceDescriptor *identity; void (*update)(DataSource source, TypeMetadata *Self); };
-
31:20 - A C function signature roughly approximating how generic functions receive generic parameters
void updateAll(Array<Model> models, DataSource source, TypeMetadata *Model, DataModelWitnessTable *Model_is_DataModel);
-
31:36 - A function that receives an array of values of protocol type
protocol DataModel { func update(from source: DataSource) } func updateAll(models: [any DataModel], from source: DataSource)
-
31:49 - A C struct roughly approximating the layout of the Swift type `any DataModel`
struct AnyDataModel { OpaqueValueStorage value; TypeMetadata *valueType; DataModelWitnessTable *value_is_DataModel; }; struct OpaqueValueStorage { void *storage[3]; };
-
31:50 - A contrast of the two Swift function signatures from before
protocol DataModel { func update(from source: DataSource) } func updateAll<Model: DataModel>(models: [Model], from source: DataSource) { for model in models { model.update(from: source) } } func updateAll(models: [any DataModel], from source: DataSource) { for model in models { model.update(from: source) } }
-
32:57 - Specialization of a generic function for known type parameters
func updateAll<Model: DataModel>(models: [Model], from source: DataSource) { for model in models { model.update(from: source) } } var myModels: [MyDataModel] updateAll(models: myModels, from: source) // Implicitly generated by the optimizer func updateAll_specialized(models: [MyDataModel], from source: DataSource) { for model in models { model.update(from: source) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。