调试性能问题

概览

本文档旨在帮助您调试 Swift 中的性能问题,通过识别和解决代码中可能导致应用程序运行缓慢或消耗过多系统资源的任何瓶颈或低效之处。通过调试性能问题,您可以优化代码并提高 Swift 应用程序的整体速度和效率。

以下是一些调试 Swift 中性能问题的基本方法和工具

  1. 测量性能Xcode 的 InstrumentsLinux perf 提供了性能分析工具,用于跟踪应用程序的性能,并帮助识别消耗过多 CPU、内存或能源的区域。例如,性能分析和火焰图显示 CPU 的消耗,而内存图显示内存的消耗。重要的是要注意,每个平台管理应用程序性能测量的方式都不同。

  2. 分析内存使用情况:使用 Xcode 的 Memory Graph Debugger 来识别和修复与内存相关的问题。

  3. 基准测试和衡量改进:持续迭代和优化,直到达到所需的性能。

提示:我们建议在 release 模式下编译 Swift 代码,以确保最佳性能。debug 和 release 版本之间的性能差异非常显著。您可以通过在配置代码以收集数据之前运行命令 swift build -c release 来实现此目的。

工具

调试性能问题有时可能是一个复杂且迭代的过程。它需要技术、工具和分析的结合。我们整理了一些工具和方法,以帮助您有效地识别和解决瓶颈,例如

火焰图

火焰图 是分析程序性能的有用工具。它们显示了程序中哪些部分占用时间最多,这可以帮助您找到需要改进的区域。

Xcode 中的火焰图

虽然 Xcode 中没有专门用于创建像 Linux perf 那样的火焰图的内置工具,但您可以使用外部工具为使用 Xcode 开发的某些应用程序生成火焰图。

Instruments 是 Xcode 的一部分,是创建火焰图的一种常用工具。您可以使用 Instruments 中的 Time Profiler 工具捕获堆栈,并使用诸如 flamegraph.pl 等工具将捕获的数据转换为火焰图。使用 Instruments 和 Time Profiler 运行应用程序,然后将收集的数据转换为火焰图,可以帮助您深入了解应用程序的性能概况。

Linux 中的火焰图

火焰图可以在大多数平台上创建,包括 Linux 上的 Swift。在本节中,我们将重点介绍 Linux。

为了讨论,这里有一个 火焰图程序示例 (在 Linux 上),它使用了 TerribleArray 数据结构,导致低效的 O(n) 追加,而不是预期的 O(1) 摊销时间复杂度(对于 Array 而言)。这可能会导致性能问题并影响程序的整体效率。

/* a terrible data structure which has a subset of the operations that Swift's
 * array does:
 *  - retrieving elements by index
 *     --> user's reasonable performance expectation: O(1)   (like Swift's Array)
 *     --> implementation's actual performance:       O(n)
 *  - adding elements
 *     --> user's reasonable performance expectation: amortised O(1)   (like Swift's Array)
 *     --> implementation's actual performance:       O(n)
 *
 * ie. the problem I'm trying to demo here is that this is an implementation
 * where the user would expect (amortised) constant time access but in reality
 * is linear time.
 */
struct TerribleArray<T: Comparable> {
    /* this is a terrible idea: storing the index inside of the array (so we can
     * waste some performance later ;)
     */
    private var storage: Array<(Int, T)> = Array()

    /* oh my */
    private func maximumIndex() -> Int {
        return (self.storage.map { $0.0 }.max()) ?? -1
    }

    /* expectation: amortised O(1) but implementation is O(n) */
    public mutating func append(_ value: T) {
        let maxIdx = self.maximumIndex()
        self.storage.append((maxIdx + 1, value))
        assert(self.storage.count == maxIdx + 2)
    }

    /* expectation: O(1) but implementation is O(n) */
    public subscript(index: Int) -> T? {
        get {
            return self.storage.filter({ $0.0 == index }).first?.1
        }
    }
}

protocol FavouriteNumbers {
    func addFavouriteNumber(_ number: Int)
    func isFavouriteNumber(_ number: Int) -> Bool
}

public class MyFavouriteNumbers: FavouriteNumbers {
    private var storage: TerribleArray<Int>
    public init() {
        self.storage = TerribleArray<Int>()
    }

    /* - user's expectation: O(n)
     * - reality O(n^2) because of TerribleArray */
    public func isFavouriteNumber(_ number: Int) -> Bool {
        var idx = 0
        var found = false
        while true {
            if let storageNum = self.storage[idx] {
                if number == storageNum {
                    found = true
                    break
                }
            } else {
                break
            }
            idx += 1
        }
        return found
    }

    /* - user's expectation: amortised O(1)
     * - reality O(n) because of TerribleArray */
    public func addFavouriteNumber(_ number: Int) {
        self.storage.append(number)
        precondition(self.isFavouriteNumber(number))
    }
}

let x: FavouriteNumbers = MyFavouriteNumbers()

for f in 0..<2_000 {
    x.addFavouriteNumber(f)
}

生成火焰图

要在 Linux 上的 Swift 中生成火焰图,您可以使用各种工具,例如 perf 结合 FlameGraph 脚本,以收集有关 CPU 使用率和堆栈跟踪的数据。然后可以使用火焰图工具将它们可视化,以深入了解应用程序的性能特征,如下所示

  1. 安装和配置 perf 以在 Linux 上收集性能数据。
  2. 使用以下步骤,使用 swift build -c release 将代码编译为名为 ./slow 的二进制文件

    a. 打开终端并导航到包含 Swift 代码的目录,通常是 Swift 包的根目录。

    b. 运行以下命令以在 release 模式下编译代码,从而优化构建以获得性能

     swift build -c release
    

    构建过程成功完成后,您可以在 Swift 包目录中的 .build/release/ 目录中找到编译后的二进制文件。

    c. 将编译后的二进制文件复制到当前目录,并使用以下命令将其重命名为 slow

     cp .build/release/YourExecutableName ./slow
    

    YourExecutableName 替换为已编译二进制文件的实际名称。

  3. 使用此命令在 ~/FlameGraph 目录中克隆存储库
     git clone https://github.com/brendangregg/FlameGraph
    
  4. 运行此命令以 99 Hz 采样频率记录堆栈帧
     sudo perf record -F 99 --call-graph dwarf -- ./slow
    

或者,要附加到现有进程,请使用: sudo perf record -F 99 --call-graph dwarf -p PID_OF_SLOW

  1. 通过运行此命令将记录导出到 out.perf
     sudo perf script > out.perf
    
  2. 使用此命令聚合记录的堆栈并反解符号
     ~/FlameGraph/stackcollapse-perf.pl out.perf | swift demangle > out.folded
    
  3. 使用以下命令将结果导出到 SVG 文件,以直观地表示函数及其相对 CPU 使用率
     ~/FlameGraph/flamegraph.pl out.folded > out.svg # Produce
    

生成的火焰图文件应类似于下图

Flame graph

我们可以在火焰图中看到 isFavouriteNumber 消耗了大部分运行时,它是从 addFavouriteNumber 调用的。此结果指示了需要改进的地方。

注意:如果您使用 Set<Int> 存储 FavouriteNumber,则副产品应指示数字是否为恒定时间 (O(1)) 的 FavouriteNumber

Malloc 库

在 Swift 中,内存分配和释放主要由 自动引用计数 (ARC) 机制管理。在某些情况下,您可能需要使用 malloc 库与其他语言(例如 C)进行交互,或者您需要对内存管理进行更精细的控制。例如,您可以为对内存分配子系统施加巨大压力的工作负载使用自定义 malloc 库。虽然无需更改代码,但在运行服务器之前,需要通过环境变量进行介入。

提示:您可能需要对默认内存分配器和自定义内存分配器进行基准测试,以了解它对指定工作负载的帮助程度。

以下是一些专门的内存分配库,旨在解决性能问题,尤其是在多线程环境中

还存在其他 malloc 实现,通常可以使用 LD_PRELOAD 启用

> LD_PRELOAD=/usr/bin/libjemalloc.so  myprogram

这些库之间的选择取决于应用程序或系统的特定性能需求和特性。

总而言之,使用性能工具调试 Swift 服务器应用程序有助于优化性能、增强用户体验、规划可伸缩性,并确保服务器应用程序在生产环境中的高效运行。