介绍 Benchmark Package:使用性能检查补充单元测试
在软件开发领域,古老的格言“先使其工作,再使其正确,最后使其快速”是创建健壮、高效应用程序的指导原则。这个旅程始于确保我们的代码按预期运行,单元测试和集成测试已证明在这项任务中不可或缺。然而,确保功能性只是等式的一部分。衡量应用程序卓越性的真正标准延伸到其性能——在各种条件下运行的速度和效率。这其中就蕴含着关键但经常被忽视的第三步:使其快速。
在专业交易软件领域,与持续集成 (CI) 集成的全面基准测试框架的作用,与单元测试和集成测试的重要性不相上下。正如单元测试和集成测试对于确保软件的功能正确性至关重要一样,CI 管道中的基准测试对于持续验证非功能性方面(例如高吞吐量、低延迟、可预测的性能和一致的资源使用)至关重要。这对于在快节奏的金融环境中保持竞争优势至关重要,在这样的环境中,极端的市场数据速率和性能要求意味着,即使是微秒级的响应时间的微小变化,也可能对交易结果产生重大影响。
无论应用程序领域如何,性能都是整体产品的重要组成部分,没有最终用户愿意等待计算机或其他电子设备,对用户操作的即时响应确实有助于提供令人愉悦的最终用户体验。
在检查了 Swift 生态系统内的现有基础设施后,我们得出结论,没有现有的解决方案能够满足我们对多平台和丰富指标支持、CI 集成以及开发者友好的需求。因此,我们决定开发一个 Benchmark package 并开源它,相信它可以帮助提升 Swift 社区的性能,并使我们所有人受益。
基准测试的作用
您是否曾经遇到过性能问题,该问题溜到了最终用户手中,并导致了错误报告?当您更改 Swift 软件包时,您是否系统地测量和验证性能指标?
Swift 的目标是达到媲美 C 语言的性能,强调可预测和一致的执行。实现这一目标涉及优化受限资源(如 CPU、内存和网络带宽)的使用,这些资源会显着影响服务器端、桌面和移动环境中的应用程序工作负载。关键性能指标包括 CPU 使用率、内存分配和管理、网络 I/O 和系统调用等。这些指标对于基础软件至关重要,在基础软件中,控制资源使用和最小化占用空间与保持运行时性能同样重要。Benchmark package 随时支持这些指标,以及 Linux 和 macOS 的特定于操作系统的指标,为 Swift 开发者提供了一个全面的工具包,以监控和增强其应用程序的效率。
构建一组基准测试并持续运行它们,可以指示何时出现性能未达到预期的情况,就像单元测试标记某些功能预期被破坏一样。然后,使用补充工具(例如 Instruments、DTrace、Heaptrack、Leaks、Sample 等)进行根本原因分析,以分析和修复根本问题。
这类似于单元测试,其中失败的测试表明存在问题,而其他更专业的工具用于修复问题(例如,调试器、TSAN/ASAN、添加断言、调试打印输出等)。
基准测试基础设施
开源的 Benchmark package 帮助您自动化性能测试,并使个人开发者在推送更改之前轻松在本地运行快速性能验证。
Benchmark package 以 SwiftPM 命令插件的形式实现,并添加了一个专用命令来与基准测试进行交互
swift package benchmark
入门介绍信息可在 package GitHub 页面 以及 Swift Package Index DocC 文档 中找到。
一个测量 Date
性能的极简基准测试可能如下所示
import Benchmark
import Foundation
let benchmarks = {
Benchmark("Foundation-Date") { benchmark in
for _ in benchmark.scaledIterations {
blackHole(Foundation.Date())
}
}
}
它既适用于主要关注 CPU 使用率的微基准测试,也适用于更复杂的长时间运行的基准测试,并且由于使用了 HDR Histogram package,因此支持在很长一段时间内测量各种样本。
Benchmark 提供了对 广泛的内置指标集 的支持
cpuUser
- 运行测试花费的 CPU 用户空间时间cpuSystem
- 运行测试花费的 CPU 系统时间cpuTotal
- 运行测试花费的 CPU 总时间(系统 + 用户)wallClock
- 运行测试的实际时间throughput
- 吞吐量,单位为操作数/秒peakMemoryResident
- 常驻内存使用量 - 在运行时采样peakMemoryResidentDelta
- 常驻内存使用量 - 在运行时采样(不包括基准测试基线的开始)peakMemoryVirtual
- 虚拟内存使用量 - 在运行时采样mallocCountSmall
- 根据 jemalloc 的说法,小型 malloc 调用的次数mallocCountLarge
- 根据 jemalloc 的说法,大型 malloc 调用的次数mallocCountTotal
- 根据 jemalloc 的说法,malloc 的总次数allocatedResidentMemory
- 根据 jemalloc 的说法,应用程序分配的常驻内存量(不包括分配器元数据开销等)memoryLeaked
- 常驻内存中小型+大型 mallocs - 小型+大型 frees 的数量(仅是可能的泄漏)syscalls
- 测试期间发出的系统调用次数 – 仅限 macOScontextSwitches
- 测试期间发生的上下文切换次数 – 仅限 macOSthreads
- 测试过程中的最大线程数(不精确,已采样)threadsRunning
- 实际在测试下运行的线程数(不精确,已采样)– 仅限 macOSreadSyscalls
- 执行的 I/O 读取系统调用次数,例如 read(2) / pread(2) – 仅限 LinuxwriteSyscalls
- 执行的 I/O 写入系统调用次数,例如 write(2) / pwrite(2) – 仅限 LinuxreadBytesLogical
- 从存储读取的字节数(可能来自 pagecache!)– 仅限 LinuxwriteBytesLogical
- 写入存储的字节数(可能已缓存)– 仅限 LinuxreadBytesPhysical
- 从块设备物理读取的字节数 – 仅限 LinuxwriteBytesPhysical
- 物理写入块设备的字节数 – 仅限 LinuxretainCount
- retain 调用的次数 (ARC)releaseCount
- release 调用的次数 (ARC)retainReleaseDelta
-abs(retainCount - releaseCount)
- 如果此值非零,则通常意味着基准测试存在 retain 循环(使用 Memory Graph Debugger 进行故障排除)
自定义指标也受支持,用于特定于应用程序的测量(例如,缓存命中/未命中统计信息)。
编写基准测试
一个稍微复杂的基准测试,用于测量 Histogram
package 的一部分
import Benchmark
import Foundation
import Histogram
let benchmarks = {
// Minimal benchmark with default settings
Benchmark("Foundation-Date") { benchmark in
for _ in benchmark.scaledIterations {
blackHole(Foundation.Date())
}
}
// Slightly more complex with some customization
let customBenchmarkConfiguration: Benchmark.Configuration = .init(
metrics: [
.wallClock,
.throughput,
.syscalls,
.threads,
.peakMemoryResident
],
scalingFactor: .kilo
)
Benchmark("ValueAtPercentile", configuration: customBenchmarkConfiguration) { benchmark in
let maxValue: UInt64 = 1_000_000
var histogram = Histogram<UInt64>(highestTrackableValue: maxValue,
numberOfSignificantValueDigits: .three)
for _ in 0 ..< 10_000 {
blackHole(histogram.record(UInt64.random(in: 10 ... 1_000)))
}
let percentiles = [0.0, 25.0, 50.0, 75.0, 80.0, 90.0, 99.0, 100.0]
benchmark.startMeasurement() // don't measure the setup cost above
for i in benchmark.scaledIterations {
blackHole(histogram.valueAtPercentile(percentiles[i % percentiles.count]))
}
benchmark.stopMeasurement()
}
}
基准测试输出和分析
默认输出为表格格式,以便于人类阅读,但该 package 支持一系列不同的输出格式,其输出适用于使用其他可视化工具进行分析。
运行基准测试时的示例默认输出:
支持关键基准测试工作流程
- 自动拉取请求性能回归检查,通过将拉取请求的性能指标与主分支进行比较,并在每个基准测试指定的绝对或相对阈值存在回归时使 PR 工作流程检查失败
- 自动拉取请求检查与预先录制的绝对基线 p90 阈值(例如,有关此类工作流程和 相关 Docker 文件,请参阅 Swift Certificates),适用于例如 malloc 回归测试
- 手动比较多个性能基线,供个人开发者进行迭代或 A/B 性能工作
- 以多种格式导出基准测试结果,用于分析或可视化
- 直接从 Xcode 在基准测试套件可执行文件上运行 Instruments profiler
结束语
包括 Swift Foundation、SwiftPM、SwiftNIO 和 Google Flatbuffers 等主要公共项目在内的 Swift 社区最近已开始使用 Benchmark package,以专注于性能优化。
通过浏览详尽的文档,并加入 Swift 论坛上的对话,分享见解并获得您的问题的答案,来了解如何将此工具用于您自己的 Swift 应用程序。或者为什么不为您最喜欢的缺少性能测试的开源 package 提供 PR 呢?
立即采取第一步来改进您的软件,添加其第一个基准测试以检查性能!