Optimize CPU performance with Instruments
Graphics & Games 高级 2m

用 Instruments 优化 CPU 性能

Optimize CPU performance with Instruments

2025年6月9日

在 Apple 官方观看视频

一句话判断

一个二分查找函数从 Collection 换到 Span 就快了 4 倍,手动特化泛型再快 1.7 倍,无分支化再快 2 倍,Eytzinger 布局再快 2 倍——总共 25 倍加速,靠的不是灵感,是 Instruments 的三层工具链。

这场 Session 讲了什么

Session 用一个二分查找的性能优化作为贯穿案例,演示了 Instruments 的三层性能分析工具:CPU Profiler(采样式分析,定位软件级开销)、Processor Trace(完整指令追踪,1% 开销的无采样偏差分析)、CPU Counters(硬件计数器,分析 CPU 微架构瓶颈)。

这不是一个”用 Time Profiler 看看热点函数”的入门 Session。它深入到了 CPU 的指令流水线、分支预测、缓存层次,讲的是”为什么这段代码在 CPU 上跑不快”。Session 还强调了一个重要的性能优化心态:先确认”这工作是否必须做”(删代码、延迟执行、预计算),再考虑”怎么做得更快”。

值得深挖的点

CPU Profiler vs Time Profiler:别再用错工具

Time Profiler 基于定时器周期性采样 CPU 调用栈。问题是 aliasing——如果你的代码和采样器有相同的周期节奏,会严重过采样或欠采样。CPU Profiler 基于每个 CPU 的时钟频率独立采样,Apple Silicon 的大小核频率不同,CPU Profiler 会自动对应采样密度。结论:做 CPU 优化时,永远用 CPU Profiler 而不是 Time Profiler

Processor Trace:无采样偏差的完整指令追踪

这是 Session 最重磅的新工具。Processor Trace 记录进程在用户空间执行的每一条指令,无采样偏差,只有约 1% 的性能开销。需要 M4 Mac/iPad Pro 或 A18 iPhone 支持。

展示方式是火焰图,但和采样火焰图不同——它显示的是实际的函数调用序列和精确的 cycle 数。你可以在几百纳秒的单次函数调用中看清每一层调用的开销。Session 用它发现了二分查找中泛型未特化的问题——Comparable 参数导致每次比较都走 protocol witness,占了总 cycle 数的 25%。这个问题在采样式分析中被淹没在噪音里,Processor Trace 一眼就看到了。

瓶颈分析(Bottleneck Analysis):从”哪里慢”到”为什么慢”

CPU Counters 今年新增了预设模式,走的是引导式分析流程:

  1. CPU Bottlenecks 模式:把 CPU 工作分成四个大类——指令分发、指令处理、分支预测丢弃、缓存未命中。Session 的二分查找在”分支预测丢弃”上占比很高。
  2. Discarded Sampling 模式:采样导致分支预测失败的具体指令。定位到二分查找中 needle < middle 的比较——因为搜索路径是随机的,分支预测器无法预测这个条件。
  3. 无分支化重写:用条件移动指令(conditional move)替代条件跳转,消除分支预测失败。代价是代码可读性下降,需要 unchecked arithmetic 避免额外分支。
  4. L1D Cache Miss Sampling:无分支化后,瓶颈转移到指令处理 → 进一步分析发现是 L1 数据缓存未命中。二分查找的随机访问模式是缓存的天敌——每一步都跳到完全不同的内存位置。
  5. Eytzinger 布局:重新排列数组,使二分查找的前几步落在同一条缓存行上。代价是顺序遍历变慢,但搜索快了 2 倍。

性能优化的心态

Session 开头花了不少篇幅讲”性能心态”,几个关键点:

  • 保持开放心态:瓶颈可能在意想不到的地方——可能是阻塞等待而非 CPU 计算,可能是 API 误用(QoS 等级错误),可能是隐式创建了过多线程。
  • 先问”这工作能不能不做”:删除代码 > 延迟执行 > 预计算 > 缓存 > 最后才是微优化。
  • 每次改动后回到初始瓶颈模式验证:优化可能解决一个瓶颈但引入新的。
  • 知道何时停止:当优化不再影响关键路径性能时就该停手。

代码片段

1. 从 Collection 到 Span:4 倍提速

场景:二分查找的泛型容器从 Collection 换成连续内存的 Span。

// ❌ Collection 版本:协议见证、泛型开销、可能的分配
func binarySearch<Haystack: Collection>(
    needle: Haystack.Element, in haystack: Haystack
) -> Haystack.Index? where Haystack.Element: Comparable {
    // ... 协议调度开销大
}

// ✅ Span 版本:连续内存、无协议开销
func binarySearch(needle: Int, in haystack: Span<Int>) -> Int? {
    var start = 0
    var length = haystack.count
    while length > 0 {
        let half = length / 2
        let middle = start + half
        if haystack[middle] < needle {
            start = middle + 1
            length -= half + 1
        } else if haystack[middle] == needle {
            return middle
        } else {
            length = half
        }
    }
    return nil
}

坑:Span 防止内存引用逃逸——不能跨函数持有 Span 引用。仅在元素连续存储时适用。

2. 无分支二分查找:消除分支预测失败

场景:分支预测器无法预测搜索路径的随机条件。

// 用条件移动替代条件跳转
func branchlessBinarySearch(needle: Int, in haystack: Span<Int>) -> Int? {
    var base = 0
    var length = haystack.count
    while length > 1 {
        let half = length / 2
        // 条件移动:不做控制流分支
        if haystack[base + half] <= needle {
            base = base + half // &+ 如果用 unchecked
        }
        length = half
    }
    return haystack[base] == needle ? base : nil
}

坑:无分支化依赖编译器生成条件移动指令(cmov),可能被编译器优化打断。用 Processor Trace 确认生成的指令是否符合预期。此版本简化了示例,实际中需要用 unchecked arithmetic 避免溢出检查分支。

最佳实践

写性能测试再优化。Session 用了一个简单的循环测试:跑一秒、统计迭代次数、用 OS signpost 标记区间。不需要多精确,只要能对比”改之前 vs 改之后”就行。ContinuousClock 比 Date 适合做性能计时(不会回退,低开销)。

按工具粒度递进:先用 CPU Profiler 找到软件级热点(函数调用开销),再用 Processor Trace 确认有没有隐藏的间接调用开销(泛型未特化、协议分派),最后用 CPU Counters 做微架构优化(分支预测、缓存)。不要跳步——微架构优化之前先确保没有软件级开销在干扰。

微优化要谨慎。无分支化、Eytzinger 布局这些技巧收益大但可维护性差。只在性能关键路径上用,并且用注释和文档说明为什么这么做。每次编译器/系统更新后都应该重新验证——编译器可能不再生成你期望的指令序列。

还有什么值得关注

  • Span 类型(Session 提到的 “Improve memory usage and performance with Swift”):Swift 新增的连续内存视图类型,是很多性能优化的基础。
  • Apple Silicon CPU Optimization Guide:Apple 官方文档,深入讲 Apple Silicon 的微架构特性,做底层优化必读。
  • 系统级阻塞分析:如果 CPU Profiler 显示线程大量时间 off-CPU,用 System Trace 分析阻塞原因(文件 I/O、锁竞争等),Session “Visualize and optimize Swift concurrency” 覆盖了这个话题。
  • Processor Trace 需要 M4/A18 以上设备:如果你手头没有这些设备,CPU Profiler + CPU Counters 已经能覆盖大部分场景。
图形与游戏 机器学习 开发工具