View in English

  • 打开菜单 关闭菜单
  • Apple Developer
搜索
关闭搜索
  • Apple Developer
  • 新闻
  • 探索
  • 设计
  • 开发
  • 分发
  • 支持
  • 账户
在“”范围内搜索。

快捷链接

5 快捷链接

视频

打开菜单 关闭菜单
  • 专题
  • 相关主题
  • 所有视频
  • 关于

更多视频

  • 简介
  • 概要
  • 转写文稿
  • 代码
  • 优化 Swift 代码的内存使用和性能

    了解如何提升 Swift 代码的性能和内存管理。我们将探索优化代码的多种方法,包括进行高级算法更改,以及采用新的 InlineArray 和 Span 类型对内存和分配进行更精细的控制。

    章节

    • 0:00 - 简介与内容安排
    • 1:19 - QOI 格式与解析器 App
    • 2:25 - 算法
    • 8:17 - 分配
    • 16:30 - 独占性
    • 19:12 - 栈与堆
    • 21:08 - 引用计数
    • 29:52 - Swift Binary Parsing 资源库
    • 31:03 - 后续步骤

    资源

    • Performance and metrics
    • Swift Binary Parsing
    • The Swift Programming Language
    • The Swift website
      • 高清视频
      • 标清视频

    相关视频

    WWDC25

    • 分析并优化 App 的功耗
    • 通过 Instruments 优化 CPU 性能
    • Swift 的新功能

    WWDC24

    • 探索 Swift 性能
  • 搜索此视频…

    大家好! 我是 Nate Cook 负责 Swift 标准资源库的相关工作 今天我们将探讨如何了解 并提高代码的性能 尤其是通过使用 Swift 6.2 中 语言和标准资源库的一些新增内容 我们将使用新的 InlineArray 和 Span 类型 尝试值泛型 并了解不可逃逸类型 我们将使用所有这些新工具 来消除保留与释放 独占性与唯一性检查 以及其他额外的工作 我还将推出一个新的开源库 以便使用所有这些工具 来快速安全地编写二进制解析器 这个库名为 “Swift Binary Parsing” 它侧重速度并提供了用于管理 几种不同安全类型的工具 我们都希望代码快速运行 Swift 提供了实现这一目标的工具 但有时 速度并不像我们预期的那么快 在这个讲座中 我们将练习弄清楚 运行时间都花在了哪里 然后尝试多种性能优化方式… 选择正确的算法 消除额外的分配 取消独占性检查 从堆分配改为堆栈分配 以及减少引用计数 在探索过程中 我们将分析我构建的一个小 App 它是一个针对图像 格式 QOI 的查看器 并且包含针对这种格式的手写解析器 QOI 是一种无损图像格式 它足够简单 规范说明 只有寥寥数语 因此可以用来尝试不同的方法 并查看相应性能 QOI 格式使用 二进制格式的标准习惯用法: 具有固定大小的标头 后面跟的数据分段 包括动态数量的不同大小的编码像素 编码像素有多种形式 像素可以是 RGB 或 RGBA 值、 与前一个像素的差值、 对先前所示像素的 缓存的查找 或者… 仅仅是前一个像素要重复的次数 好了 – 让我们试试 我的 QOI 解析器 App!

    我可以打开这个图标文件 它只有几 KB 因此可迅速载入

    这张鸟类的照片有点大 载入可能需要几秒钟… 打开了 为什么会花这么长时间? 当你发现处理真实世界数据的速度 明显变慢时 通常是由于 算法或数据结构的使用有误 让我们使用 Instruments 来查找 并解决这个问题的根源 在我的解析库中 我编写了一个测试来解析这张 载入缓慢的鸟类图像 我可以点按运行按钮来运行测试…

    几秒钟后测试通过 除了使用这个测试来检查正确性 我还可以在 Instruments 中 分析这个测试 了解它的性能 这一次 我对运行按钮 进行辅助点按 菜单中会出现一个 用于分析测试的选项

    我喜欢这个功能 在分析测试时 我可以专注于代码中我感兴趣的 特定部分 我现在将选择这个选项 来启动 Instruments

    Instruments 会在打开时显示模板 选择器 其中展示了可以 帮助你了解代码性能的所有不同方式 我们今天将使用两种不同的工具 所以我将从空白模板开始

    我可以通过点按“+ Instrument” 按钮来添加工具 我将添加“Allocations” 工具来帮助了解 我的解析器如何分配内存 而且由于我真的很想知道我的 App 会在哪些地方花费时间 我将添加“Time Profiler”工具

    Time Profiler 是解决 性能问题的理想起始点 让我们隐藏边栏 为结果腾出更多空间 然后使用录制按钮开始测试

    我们可以在结果窗口中看到一些内容

    性能分析中包含的工具列在窗口顶部 我们将首先使用“Time Profiler” 因此我将它保持选中状态 底部是所选工具的详细视图 左侧是已捕获调用的列表… 右侧是当前所选调用的最重堆栈跟踪

    我想先查看最常捕获的调用 无论它们是如何到达的 因此我将点按“Call Tree”按钮

    然后选中 “Invert Call Tree”复选框 我可以使用详细视图顶部的这个按钮 切换到图形视图 点按后 视图会切换为 以火焰图的形式显示性能分析

    火焰图中的每个条形都显示了 在分析过程中某个调用 被捕获次数的占比 在这个示例中 有一个巨大的 条形在整个流程中占据了主导地位 它的标记为“platform_memmove” 同样的符号也出现在堆栈跟踪中 memmove 是一个用于拷贝 数据的系统调用 因此这个 巨大的条形表示解析器 大部分时间都在拷贝数据 而不是读取数据 但情况不应该是这样 让我们找出代码的哪个部分 导致了所有这些拷贝操作 我想查看堆栈跟踪中的所有帧 因此 我将点按视图顶部的 “Show all frames”按钮

    跟踪顶部是系统调用 包括 platform_memmove 然后是由 Foundation Data 类型 提供的一些 专用版本的方法 你可能在堆栈跟踪中或在调试时 看到过类似的专用方法 这些专用方法是 Swift 编译器 为你生成的泛型代码的类型专用版本

    最后 我们来到我定义的方法 即 readByte

    由于这是代码中 最接近问题位置的函数 因此最好从这里开始 要直接跳转到这个方法 我可以使用辅助点按 然后选取“Reveal in Xcode”

    这是 Xcode 中 readByte 方法的声明 Instruments 会直接跳转到这一行 我在这里删除了第一个字节 然后调用了 Data 构造器 借助 Instruments 我能够识别 可能导致我的库运行缓慢的 所有 memmove 调用 然后直接跳转到导致 所有拷贝操作的具体代码行

    这个辅助方法非常重要… 因为我的解析代码 会在使用原始二进制数据时 反复调用 readByte

    我本以为这只会在每次调用 readByte 方法时缩减数据 返回第一个字节 并将数据的开头前移 结果却发现 每次读取一个字节时 它都会将整个数据内容 拷贝到一个新的分配空间 这比我预想的要复杂得多 让我们修复这个错误 我现在回到 Xcode 编辑 readByte 方法

    由于 Data 类型是从两端收缩 因此我们实际上可以使用一个 名为“popFirst()”的集合方法 popFirst() 会返回 “data”中的第一个字节 然后将集合的开头向前移动 将集合缩减一个字节 这正是我们想要的

    解决这个问题后 我可以切换回 我的测试 然后再次运行性能分析

    Instruments 会自动打开 且测试已采用相同的 性能分析配置 非常好! 那个巨大的 platform_memmove 条形从火焰图中消失了

    当我对代码进行基准测试时 还可以看到速度由于这一变化 而显著提升! 这太棒了 但对于这样的算法更改 速度显著提升并非全部 在我的原始版本中 图像大小与解析时间之间的关系 是二次函数 随着我解析的图像越来越大 解析所需的时间将急剧增加 修复拷贝问题后 两者之间的关系变成了线性关系 图像大小与解析时间之间的关联 更加直接 我们还将进行更多改进 以提高线性性能 并且我们将能够更直接地 比较这些改进

    解决这个问题后 让我们来看看另一个常见的 性能陷阱:额外分配 我们来看看现在最重的 堆栈跟踪是什么 这些调用表明 会有大量流量流向那些 用于分配和释放 Swift 数组的方法 分配和释放内存可能开销很大 如果我能找出这些额外分配的来源 并消除它们 我的解析器 就会更快 要查看解析器正在进行的分配 我可以使用之前添加的 Allocations 工具 有几个不同的指标表明我的代码 可能会导致不必要的分配 首先是庞大的数量: 在解析一张图片的过程中 有近百万次分配? 我认为我们可以做得更好 其次我们可以看到 几乎所有这些分配都是瞬时分配 被 Allocations 工具 标记为短期类型

    为了找到问题的根源 我会从详细信息面板 切换到“Call Tree”视图 首先 点按标记为 “Statistics”的弹出按钮 然后选择“Call Trees”

    由于已选中顶部线程 我将查看堆栈跟踪以找到 最接近问题的代码部分 由于这个堆栈跟踪没有倒序显示 我需要从跟踪的底部开始查看 解析器中的第一个符号 是这个 RGBAPixel.data 方法

    当我点按这个方法时 它会显示在调用树详细信息窗口中 在这里对这个方法进行辅助点按时 我可以选取“Reveal in Xcode” 以直接跳转到源代码

    这个方法似乎就是 造成额外分配的原因 我发现每次调用它时 它都会返回一个包含像素 RGB 值 或 RGBA 值的数组 这意味着每次调用它时 它都会创建一个数组 并为至少三个元素分配空间

    为了找出它的使用位置 我将对函数名称进行辅助点按 然后选取“Show callers”

    调用方是主解析函数中的这个闭包 它只是这个巨大的 flatMap 和 prefix 链的一部分 为了理解这段代码为何 会产生如此多的独立分配 让我们来一步一步了解 这些分配是如何堆积起来的

    首先 readEncodedPixels 方法 将二进制数据解析为编码像素 也就是我之前提到的不同像素类型 并且需要分配足够的空间来存储它们

    接下来 为每个编码像素 调用 decodePixels 以生成一个或多个 RGBA 像素 大多数编码最终只会生成一个像素 但有一种编码 代表我们需要将前一个 像素重复一定次数 为了支持这一点 decodePixels 会始终返回一个数组 而每个数组都需要分配内存

    flatMap 的“扁平化”部分 会接受我们刚刚创建的 所有这些小数组并将它们 合并成一个更大的数组 这是一种新的分配 我们刚刚创建的所有 小数组都会被释放

    这个 prefix 方法 对我们可以生成的 像素数量设置了上限

    第二个 flatMap 首先 调用 RGBAPixel.data 这是我们在使用 Allocations 工具时标记的方法 我们之前看到它返回一个 包含三个或四个元素的数组 我们现在看到的内容意味着 最终图像中的每个像素 都会创建一个三元素或四元素数组 有时编译器能够优化掉 其中一些额外的分配 但正如我们在跟踪中看到的 这种情况并不总是会发生

    接下来 这些小数组再次扁平化 形成一个大的新数组 最后 这个包含 RGB 或 RGBA 像素数据的大数组 被拷贝到一个新的 Data 实例中 以供返回

    这些代码行有一定的优势 它们将大量功能打包到 几个简短的链式方法调用中 但更短并不意味着更快 与其执行所有这些不同的步骤 最终获得一个 可供返回的 Data 实例 不如先分配数据 然后在解码二进制源数据时 写入每个像素 这样 我们就可以执行 所有相同的处理 而无需任何这类中间分配 我将回到我的解析函数 让我们重写这个方法 以消除所有这些额外的分配

    我们首先要做的是 计算“totalBytes”:结果数据的 最终大小 然后 我们将为“pixelData” 分配恰到好处的存储空间 “offset”变量会跟踪 我们写入的数据量 这种预先分配意味着 我们无需在处理二进制数据时 进行额外的分配

    接下来 我们将解析 每一段数据并立即处理 我们可以使用 switch 语句 来处理解析的像素

    对于指示一次运行的编码像素 我们将循环所需的次数 且每次都写出像素数据

    对于任何其他类型的像素 我们将解码并将它们直接写入数据中 这是完全重写 除了我们需要返回的数据之外 没有其他分配 让我们通过再次分析测试 来验证我们是否已解决这个问题

    我们可以立即看到 分配的数量大大减少 要查看代码中实际的分配数量 我可以使用过滤器 我会点按窗口底部的过滤字段 然后输入“QOI.init”

    这会过滤掉 堆栈跟踪中不包含 QOI.init 的所有调用树 剩下的几行显示 现在我们的解析器代码 只进行了少量的分配 总共不到 2 MB 当我按住 Option 键并点按对应的 展开三角形时 调用树会展开

    展开后的树会显示我们想要的内容

    我们唯一真正分配的是 用于存储结果图像的 Data

    从基准测试来看 这又是一个重大改进 通过减少这些额外的分配 我们将执行时间缩短了一半以上

    到目前为止 我们已经 对解析器进行了两项算法更改 消除了大量意外拷贝 从而减少了分配数量 对于接下来的几项改进 我们将使用一些更高级的技术 从而使 Swift 编译器能够 消除运行时发生的大量自动内存 管理工作

    首先 我们来谈谈数组 和其他集合类型的工作原理 Swift 的数组类型是我们 工具箱中最常用的工具之一 因为它快速、安全且易于使用 数组可以根据需要增大或缩小 因此 你无需提前知道 要处理的项目数量 Swift 会在后台为你处理内存 数组也是值类型 这意味着 对数组一个副本的更改 不会影响其他副本 如果你拷贝一个数组 例如将它赋值给其他变量 或将它传递给函数 Swift 不会立即复制其中的元素 而是会使用一种 称为“写时拷贝”的优化机制 将拷贝操作延迟到 你实际更改其中一个数组时再执行

    这些特性使数组成为了 一个很棒的通用型集合 但它们也有一些缺点 为了支持动态大小和多个引用 数组将自身内容存储在 单独的分配中 通常是在堆上 Swift 运行时使用引用计数 来跟踪每个数组的副本数 当你进行更改时 数组会进行唯一性检查 以了解是否需要拷贝自身元素 最后 为了确保代码安全无虞 Swift 会强制实现独占性 这意味着两个不同的对象 不能同时修改同一数据 虽然这条规则通常是在 编译时强制执行的 但有时只能在运行时强制执行 现在我们已经了解了这些底层概念 让我们看看它们是如何 在我们的性能分析中体现的 我们首先来查找一下 运行时的独占性检查 这种检查会增加程序的工作量 并妨碍优化 在开始查找独占性检查之前 我们实际上有一个幸福的烦恼 我们充分提升了性能 以至于 Instruments 没有足够的 时间来检查解析器进程 我们可以通过循环执行 解析代码来增加分析内容 50 次应该足够了

    让我们来看看这个更丰富的性能分析

    独占性测试在跟踪中 显示为“swift_beginAccess” 和“swiftendAccess”符号 我将再次点按窗口底部的过滤框 然后输入符号名称

    在火焰图的顶部 swift_beginAccess 出现了几次 且需要进行这项检查的符号 就显示在下方 这些符号是前一个像素和 像素缓存的访问器 它们存储在解析器的 State 类中 我会切换回 Xcode 并找到这个声明 显示出来了… State 是一个类 具有我们 在火焰图中看到的两个属性 修改类实例是 Swift 必须 在运行时检查独占性的情况之一 因此 这个声明是导致 我们所见结果的原因 我们可以将这些属性移出类 并直接放入解析器类型中 从而免去这种检查

    接下来 我们将执行查找替换操作 来移除针对 previousPixel 和 pixelCache 的“state.”访问

    当我构建时 编译器会告诉我 还有一些工作要做

    由于状态属性不再嵌套在类中 我无法使用非变异方法修改它们

    我将接受这个修复 以便将方法设为变异方法

    还有一个问题需要修复…

    好了 完成这一更改后 让我们回到测试

    然后重新录制性能 分析过程来查看变化

    我将再次过滤出 swift_beginAccess

    这次什么都没有! 我们已完全消除运行时独占性检查 让我们再来看看那些状态变量 借助这种变量 可以使用 Swift 的新功能 将数据从堆内存移动到堆栈内存 并确保那些独占性检查不会再次出现 解析器中的 pixelCache 是一个 RGBAPixel 数组 它会初始化为 64 个元素 并且永远不会改变大小 这个缓存非常适合 使用新的 InlineArray 类型 InlineArray 是 Swift 6.2 中 一种新的标准资源库类型 像常规数组一样 它将多个相同类型的元素 存储在连续的内存中 但二者之间存在一些重要区别 首先 内联数组的大小是固定的 需要在编译时设置 与可以追加或移除元素的 常规数组不同的是 InlineArray 使用新的值泛型特性 会将大小作为类型定义的一部分 这意味着 你虽然可以更改内联数组的元素 但无法追加或移除元素 也无法 将内联数组赋值给其他大小的数组

    其次 顾名思义 当你使用 InlineArray 时 元素将始终以内联方式存储 而不是存储在单独的分配中 内联数组不会在副本之间 共享存储空间 也不会使用写时拷贝 相反 每次你进行拷贝时 它们都会被拷贝 这免去了常规数组 所需的所有引用计数 以及唯一性和独占性检查 InlineArray 这种不同的拷贝行为 有点像一把双刃剑 如果数组的使用需要 在不同变量或类之间 进行拷贝或共享引用 InlineArray 可能并非正确选择 但在这个示例中 像素缓存是一个固定大小的数组 它会被就地修改 但永远不会被拷贝 非常适合使用“InlineArray”

    对于我们的最后一项优化 我们将使用标准 资源库的新 span 类型 消除解析时的大部分引用计数 回到 Time Profiler 火焰图 让我们再次使用过滤功能 来只查看我们的 QOI 解析器 我将在过滤框中添加 QOI.init

    视图将更改为仅关注包含解析 构造器的堆栈跟踪 让我们来查找保留和释放符号 swift_retain 是这个粉红色条形 出现在 7% 的样本中 swift_release 是这个 出现在另外 7% 的样本中 我们之前讨论的唯一性检查 也显示在这里 出现在另外 3% 的样本中

    为了弄清楚它们出现的原因 我将再次点按 swift_release 并像之前所做的那样 向下扫描最重的堆栈跟踪 以找到第一个由用户定义的方法 问题似乎还是出在 我们一开始提到的 readByte 方法

    这一次 我们要处理的不是算法问题 而是“Data”本身的使用问题 与“Array”类似 “Data”通常将内存存储在堆上 并且需要进行引用计数 这些引用计数操作 (保留和释放) 非常高效 但如果是在紧凑循环中执行 则可能会耗费大量时间 本例中的这个方法就是这种情况 为了解决这个问题 我们想要 从使用“Data”或“Array”等 高级集合类型 转变为使用不会导致 引用计数激增的类型 在 Swift 6.2 之前 你可能使用过类似 “withUnsafeBufferPointer” 的方法来访问集合的底层存储 借助这些方法 你可以手动管理内存 而无需进行引用计数 但它们会给代码带来安全风险

    值得一问的是 为什么指针不安全? Swift 说它们不安全 是因为它们绕过了 这种语言的许多安全保证 它们可以指向已初始化 和未初始化的内存 它们会放弃某些类型保证 并且可以脱离上下文 从而导致有可能访问 已不再分配的内存 当你使用不安全的指针时 你需要全权负责代码的安全性 编译器帮不了你 这个 processUsingBuffer 函数 确实正确使用了不安全的指针 这种使用完全在不安全的 缓冲区指针闭包内进行 最后仅返回计算结果 另一方面 这个“getPointerToBytes()” 函数很危险 它包含两个重大编程错误 这个函数会创建一个字节数组 并调用 withUnsafeBufferPointer 方法 但它没有将指针的使用限制在闭包内 而是将指针返回到外部作用域 这是第 1 个错误 更糟糕的是 代码随后会 从函数本身返回不再有效的指针 这是第 2 个错误! 这两个错误都将使指针的生命周期 超出它所指向内容的生命周期 从而导致出现对已移动 或释放的内存的危险遗留引用 为了解决这个问题 Swift 6.2 引入了一组 名为 Span 的新类型 Span 是一种新推出的方法 用于处理属于同一集合的连续内存 重要的是 Span 使用了新的 “不可逃逸”语言特性 这使得编译器可以将它们的生命周期 与提供它们的集合绑定 可借助 Span 访问的内存 将保证生命周期 与 Span 本身一样长 绝不会产生任何延时引用 由于每个 Span 类型 都会声明为不可逃逸类型 因此编译器会阻止你逃逸 或返回超出相应检索上下文的 Span 这个“processUsingSpan”方法 展示了如何使用 Span 编写比指针 更简单、更安全的代码 要获取数组元素的 Span 只需使用 span 属性 由于未使用闭包 我们可以访问数组的存储空间 这种方式的效率完全 不逊于不安全的指针 但却没有任何风险 如果我们尝试重写之前的危险函数 就能看到不可逃逸 语言特性的实际作用 我们首先会遇到的问题是 我们甚至无法用“Span” 编写相同的函数签名 由于 Span 的生命周期 与提供它的集合绑定 因此如果没有传入任何集合或 Span 就无处获取传出的 Span 的生命周期

    那如果我们试图通过在闭包中捕获 Span 来向编译器隐藏它呢 在这个函数中 我将创建一个数组 访问它的 Span 然后尝试返回一个 捕获这个 Span 的闭包 但即使这样也行不通 编译器认识到 捕获 Span 会导致 Span 逃逸 并指出它的生命周期取决于局部数组 这个由编译器检查的要求 即 Span 不能逃逸对应的作用域 意味着无需进行保留和释放 我们获得了使用不安全缓冲区的性能 而又避免了所有的不安全因素 “Span”类型簇包含只读和可变 Span 的类型化 及原始版本 用于处理现有集合 包括可用于初始化 新集合的输出 Span 还包括 UTF8Span 一种专为实现安全高效的 Unicode 处理而设计的新类型

    回到我们的代码 让我们为 RawSpan 实现这一相同的 readByte 方法

    首先 我们将添加一个 RawSpan 扩展…

    并定义 readByte 方法

    RawSpan 的 API 与 Data 略有不同 但它的作用与我们 上面的实现相同 它会载入第一个字节 缩减 RawSpan 然后返回载入的值 请注意 这个 unsafeLoad 方法 之所以这样命名 只是因为载入某些类型可能不安全 像我们这里这样 载入内置 整数类型一定是安全的

    接下来 我们将更新解析方法

    这两个解析方法都应该 使用 RawSpan 作为参数 而不是使用 Data

    我还需要在调用处进行一些更改

    我们不再传递数据本身 而是获取数据的 RawSpan 并将它传递给解析方法 我将使用“bytes”属性 访问 Data 的 RawSpan 这个 rawBytes 值为不可逃逸值 我无法通过这个函数返回它 但可以毫无问题地 将它传递给解析方法

    完成这项更改后 我便完成了 使用 RawSpan 所需的更新 为了节省更多底层工作 我们还可以在解析方法中 采用新的 OutputSpan

    我们不再创建零初始化的 Data 而是使用新的 rawCapacity 构造器 它会提供一个 OutputSpan 来逐步填充未初始化的数据

    OutputSpan 会跟踪 你已写入的数据量 因此我们可以使用它的 count 属性 来代替这个单独的 offset 变量

    并且 我们将使用 write-to 方法的另一种变体 将数据写入 outputSpan 而不是 Data 实例

    让我们来看看这个方法的实现

    write(to:) 方法能够 为像素中的每个通道调用 OutputSpan 的 append 方法 由于 OutputSpan 是专为这类 用途而设计的不可逃逸类型 因此这比写入到“Data”实例 更简单、更高效 比降级使用不安全的 缓冲区指针更安全 完成这些更改后 我将跳回测试 并录制新的性能分析

    我将过滤出 QOI.init

    在火焰图中 我们可以看到 swift_retain 和 swift_release 代码块消失了! 看起来真的很棒 到此为止 让我们看看采用 InlineArray 和 RawSpan 的效果

    通过这些最新更改 我们的内存管理工作 使解析速度提高了五倍 而无需使用任何不安全的代码 这比我们去除二次算法后 的速度快了 15 倍 比我们开始时快了 700 多倍! 我们在这个讲座中介绍了很多内容 在修改这个图像解析库时 我们进行了两项算法更改 以提高运行效率并减少分配 我们使用了新的标准资源库类型 InlineArray 和 RawSpan 来消除运行时内存管理 并了解了新的不可逃逸语言特性 Swift Binary Parsing 这一新库 就是基于这些特性构建的 这个库旨在构建安全、 高效的二进制格式解析器 并支持开发者处理 多种不同类型的安全问题 它提供了一整套 parsing 构造器和其他工具 可指导你安全地使用 原始二进制数据中的值

    这是使用这个新库编写的一个 QOI 标头解析器示例 这展示了库的几个特性 包括 ParserSpan 一种用于解析二进制 数据的自定原始 Span 类型 还包括 parsing 构造器 用于防止整数溢出 并允许你指定符号有无、 位宽和字节顺序 这个库还提供验证解析器 用来验证你自己的自定 ‌RawRepresentable 类型 并提供可选的生成运算符 以便使用不受信任的 新解析值安全地进行计算

    Apple 内部已在使用 Binary Parsing 库 并且这个库如今已正式发布 我们建议大家都了解并试用一下 如需加入社区 只需在 Swift 论坛中发帖或者在 GitHub 上 提问或发起拉取请求 非常感谢大家今天与我一起 优化 Swift 代码! 你可以尝试使用 Xcode 和 Instruments 就自己 App 中 性能关键型部分的测试进行性能分析 你可以阅读相关文档来了解 新的 InlineArray 和 Span 类型 也可以下载新版 Xcode 祝你在 WWDC 收获满满!

    • 7:01 - Corrected Data.readByte() method

      import Foundation
      
      extension Data {
        /// Consume a single byte from the start of this data.
        mutating func readByte() -> UInt8? {
          guard !isEmpty else { return nil }
          return self.popFirst()
        }
      }
    • 9:56 - RGBAPixel.data(channels:) method

      extension RGBAPixel {
        /// Returns the RGB or RGBA values for this pixel, as specified
        /// by the given channels information.
        func data(channels: QOI.Channels) -> some Collection<UInt8> {
          switch channels {
          case .rgb:
            [r, g, b]
          case .rgba:
            [r, g, b, a]
          }
        }
      }
    • 10:21 - Original QOIParser.parseQOI(from:) method

      extension QOIParser {
        /// Parses an image from the given QOI data.
        func parseQOI(from input: inout Data) -> QOI? {
          guard let header = QOI.Header(parsing: &input) else { return nil }
          
          let pixels = readEncodedPixels(from: &input)
            .flatMap { decodePixels(from: $0) }
            .prefix(header.pixelCount)
            .flatMap { $0.data(channels: header.channels) }
      
          return QOI(header: header, data: Data(pixels))
        }
      }
    • 12:53 - Revised QOIParser.parseQOI(from:) method

      extension QOIParser {
        /// Parses an image from the given QOI data.
        func parseQOI(from input: inout Data) -> QOI? {
          guard let header = QOI.Header(parsing: &input) else { return nil }
          
          let totalBytes = header.pixelCount * Int(header.channels.rawValue)
          var pixelData = Data(repeating: 0, count: totalBytes)
          var offset = 0
          
          while offset < totalBytes {
            guard let nextPixel = parsePixel(from: &input) else { break }
            
            switch nextPixel {
            case .run(let count):
              for _ in 0..<count {
                state.previousPixel
                  .write(to: &pixelData, at: &offset, channels: header.channels)
              }
            default:
              decodeSinglePixel(from: nextPixel)
                .write(to: &pixelData, at: &offset, channels: header.channels)
            }
          }
          
          return QOI(header: header, data: pixelData)
        }
      }
    • 15:07 - Array behavior

      var array = [1, 2, 3]
      array.append(4)
      array.removeFirst()
      // array == [2, 3, 4]
      
      var copy = array
      copy[0] = 10      // copy happens on mutation
      // array == [2, 3, 4]
      // copy == [10, 3, 4]
    • 19:47 - InlineArray behavior (part 1)

      var array: InlineArray<3, Int> = [1, 2, 3]
      array[0] = 4
      // array == [4, 2, 3]
      
      // Can't append or remove elements
      array.append(4)
      // error: Value of type 'InlineArray<3, Int>' has no member 'append'
      
      // Can only assign to a same-sized inline array
      let bigger: InlineArray<6, Int> = array
      // error: Cannot assign value of type 'InlineArray<3, Int>' to type 'InlineArray<6, Int>'
    • 20:23 - InlineArray behavior (part 2)

      var array: InlineArray<3, Int> = [1, 2, 3]
      array[0] = 4
      // array == [4, 2, 3]
      
      var copy = array    // copy happens on assignment
      for i in copy.indices {
          copy[i] += 10
      }
      // array == [4, 2, 3]
      // copy == [14, 12, 13]
    • 23:13 - processUsingBuffer() function

      // Safe usage of a buffer pointer
      func processUsingBuffer(_ array: [Int]) -> Int {
          array.withUnsafeBufferPointer { buffer in
              var result = 0
              for i in 0..<buffer.count {
                  result += calculate(using: buffer, at: i)
              }
              return result
          }
      }
    • 23:34 - Dangerous getPointerToBytes() function

      // Dangerous - DO NOT USE!
      func getPointerToBytes() -> UnsafePointer<UInt8> {
          let array: [UInt8] = Array(repeating: 0, count: 128)
          // DANGER: The next line escapes a pointer
          let pointer = array.withUnsafeBufferPointer { $0.baseAddress! }
          // DANGER: The next line returns the escaped pointer
          return pointer
      }
    • 24:46 - processUsingSpan() function

      // Safe usage of a span
      @available(macOS 16.0, *)
      func processUsingSpan(_ array: [Int]) -> Int {
          let intSpan = array.span
          var result = 0
          for i in 0..<intSpan.count {
              result += calculate(using: intSpan, at: i)
          }
          return result
      }
    • 25:07 - getHiddenSpanOfBytes() function (attempt 1)

      @available(macOS 16.0, *)
      func getHiddenSpanOfBytes() -> Span<UInt8> { }
      // error: Cannot infer lifetime dependence...
    • 25:28 - getHiddenSpanOfBytes() function (attempt 2)

      @available(macOS 16.0, *)
      func getHiddenSpanOfBytes() -> () -> Int {
          let array: [UInt8] = Array(repeating: 0, count: 128)
          let span = array.span
          return { span.count }
      }
    • 26:27 - RawSpan.readByte() method

      @available(macOS 16.0, *)
      extension RawSpan {
        mutating func readByte() -> UInt8? {
          guard !isEmpty else { return nil }
          
          let value = unsafeLoadUnaligned(as: UInt8.self)
          self = self._extracting(droppingFirst: 1)
          return value
        }
      }
    • 28:02 - Final QOIParser.parseQOI(from:) method

      /// Parses an image from the given QOI data.
      mutating func parseQOI(from input: inout RawSpan) -> QOI? {
        guard let header = QOI.Header(parsing: &input) else { return nil }
        
        let totalBytes = header.pixelCount * Int(header.channels.rawValue)
        
        let pixelData = Data(rawCapacity: totalBytes) { outputSpan in
          while outputSpan.count < totalBytes {
            guard let nextPixel = parsePixel(from: &input) else { break }
            
            switch nextPixel {
            case .run(let count):
              for _ in 0..<count {
                previousPixel
                  .write(to: &outputSpan, channels: header.channels)
              }
              
            default:
              decodeSinglePixel(from: nextPixel)
                .write(to: &outputSpan, channels: header.channels)
              
            }
          }
        }
        
        return QOI(header: header, data: pixelData)
      }
    • 28:31 - RGBAPixel.write(to:channels:) method

      @available(macOS 16.0, *)
      extension RGBAPixel {
        /// Writes this pixel's RGB or RGBA data into the given output span.
        @lifetime(&output)
        func write(to output: inout OutputRawSpan, channels: QOI.Channels) {
          output.append(r)
          output.append(g)
          output.append(b)
          
          if channels == .rgba {
            output.append(a)
          }
        }
      }
    • 0:00 - 简介与内容安排
    • 了解如何使用 Swift 6.2 优化 Swift 代码 App 和库的性能。新的 InlineArray 和 Span 类型可减少分配、独占性检查和引用计数。Swift 还引入了新的开源库 Binary Parsing,用于实现快速且安全的二进制文件解析。

    • 1:19 - QOI 格式与解析器 App
    • 在这个 WWDC25 讲座中,演示 App 加载了 QOI 格式的图像。这是一种简单的无损格式,它的规范文档仅有单页内容。这个 App 的图像解析器可处理多种像素编码方法。然后,这个 App 能瞬间加载一个小的图标文件,但加载一张较大的鸟类照片却需要几秒时间。

    • 2:25 - 算法
    • 当 App 处理真实数据时,算法或数据结构使用不当常会导致性能问题。要识别和解决这些问题,可以使用 Instruments 工具,它提供了多种工具模板来分析内存分配和释放情况,并使用性能分析器识别低效代码。 Time Profiler 工具对于分析性能问题特别有用。通过分析捕获的调用和堆栈跟踪,可以查明 App 最耗时的环节。在这个示例中,大量时间消耗在用于拷贝数据的系统调用 platform_memmove 上。 通过使用 Instruments,这个示例分析了一个名为 readByte 的自定方法。这个方法已添加到“Data”类型的扩展中,这导致了二进制数据的过度拷贝。这个示例将这个方法替换为更高效的 popFirst() 方法,后者从序列前端缩减数据而不进行拷贝。这一更改解决了 readByte 方法中的性能问题。 在做出更改后,示例再次运行性能分析,那个很大的 platform_memmove 条形从火焰图中消失了。基准测试显示速度大幅提升,并且图像大小与解析时间之间的关系从二次函数变为线性函数,表明算法效率更高。

    • 8:17 - 分配
    • 再次对 App 进行性能分析时发现,图像解析器会导致过多的内存分配和释放问题,尤其是涉及数组的操作。解析单张图像时产生近百万次内存分配,这一异常数值表明存在严重问题。这些分配大多是短暂的、生命周期极短的,这意味着它们可以进行优化。 为了找出这些不必要分配的来源,这个示例使用了 Instruments 中的 Allocations 工具。分析表明,一个名为 RGBAPixel.data(channels:) 的方法是主要原因。这个方法每次调用时都会创建一个数组,进而导致大量内存分配。 代码结构中涉及复杂的 flatMap 和 prefix 方法链,这也加剧了问题。这个链中的每个步骤都会导致新的内存分配,因为数组被重复创建、展平和拷贝。虽然这种方法很简洁,但内存效率低下。 为了解决这个问题,这个示例重写了解析函数。它不再依赖中间分配,而是预先计算结果数据的总大小并分配单个缓冲区。这种方法消除了解码过程中对重复分配的需求。

    • 16:30 - 独占性
    • App 的性能提升显著,以至于性能分析工具需要更多数据。在将解析代码循环运行 50 次后,结果中出现了 swift_beginAccess 和 swift_endAccess 符号,这些符号表明存在独占性测试。 这些独占性测试源于 QOIParser 结构体中嵌套的 State 类属性。示例随后将这些属性直接移至父解析器类型中,以消除独占性检查。经过几次编译器调整后,通过重新运行性能分析验证,独占性检查已被完全移除。

    • 19:12 - 栈与堆
    • 这个示例将 App 中的 Array 替换为 InlineArray。InlineArray 是一种内联存储的固定大小集合,通过消除引用计数和独占性检查来优化内存使用。它非常适合用作像素缓存。这个数组始终保持 64 元素大小且就地修改,无需拷贝或共享引用即可提升性能。

    • 21:08 - 引用计数
    • 在 App 的最终优化示例中,示例采用了新的 Span 类型来提升性能并增强内存安全性。Instruments 中使用了来自 Time Profiler 分析的火焰图。分析数据聚焦于 QOIParser,发现大量时间花费在引用计数操作上,特别是对于 Data 类型,这是由于它的写时拷贝语义所致。 Span 及它的相关类型是处理集合中连续内存的新方式。它们利用 Swift 的不可逃逸 (~Escapable) 特性,将生命周期绑定到集合,既确保内存安全又无需手动内存管理。这使得内存访问既高效又无需承担不安全指针的风险。 这个示例展示了如何使用 Span 类型重写现有方法,使它更简单、更安全且性能更高。在图像解析方法中,Data 被替换为 RawSpan,引用计数的开销大大降低。此外,在解析过程中采用 OutputSpan 来进一步优化,使解析操作比以前快六倍,而无需使用不安全的指针。

    • 29:52 - Swift Binary Parsing 资源库
    • Swift Binary Parsing 使你能够为二进制格式创建安全且高效的解析器。它提供了一系列工具来处理各类安全问题,包括防止整数溢出、指定符号有无、位宽和字节序,以及验证自定类型。这个库已在 Apple 内部投入使用,并已开源发布。你可以通过 Swift 论坛和 GitHub 进行试用和贡献。

    • 31:03 - 后续步骤
    • 关键要点包括: 使用 Xcode 和 Instruments 对 App 进行性能分析。 分析算法的性能以识别瓶颈。 使用 Swift 6.2 中新推出的 InlineArray 和 Span 类型来探索上述解决方案。

Developer Footer

  • 视频
  • WWDC25
  • 优化 Swift 代码的内存使用和性能
  • 打开菜单 关闭菜单
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    打开菜单 关闭菜单
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    打开菜单 关闭菜单
    • 辅助功能
    • 配件
    • App 扩展
    • App Store
    • 音频与视频 (英文)
    • 增强现实
    • 设计
    • 分发
    • 教育
    • 字体 (英文)
    • 游戏
    • 健康与健身
    • App 内购买项目
    • 本地化
    • 地图与位置
    • 机器学习
    • 开源资源 (英文)
    • 安全性
    • Safari 浏览器与网页 (英文)
    打开菜单 关闭菜单
    • 完整文档 (英文)
    • 部分主题文档 (简体中文)
    • 教程
    • 下载 (英文)
    • 论坛 (英文)
    • 视频
    打开菜单 关闭菜单
    • 支持文档
    • 联系我们
    • 错误报告
    • 系统状态 (英文)
    打开菜单 关闭菜单
    • Apple 开发者
    • App Store Connect
    • 证书、标识符和描述文件 (英文)
    • 反馈助理
    打开菜单 关闭菜单
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program (英文)
    • News Partner Program (英文)
    • Video Partner Program (英文)
    • 安全赏金计划 (英文)
    • Security Research Device Program (英文)
    打开菜单 关闭菜单
    • 与 Apple 会面交流
    • Apple Developer Center
    • App Store 大奖 (英文)
    • Apple 设计大奖
    • Apple Developer Academies (英文)
    • WWDC
    获取 Apple Developer App。
    版权所有 © 2025 Apple Inc. 保留所有权利。
    使用条款 隐私政策 协议和准则