この記事の要点
- ユニットテストやインテグレーションテストが機能の正しさを保証するのに対し、性能(速度・効率・リソース使用量)を継続的に検証する仕組みは見落とされがちです。これを埋めるためのオープンソースパッケージ Benchmark(ordo-one/package-benchmark)が発表されました。
- Benchmark は SwiftPM のコマンドプラグインとして実装されており、
swift package benchmarkコマンドでベンチマークを実行できます。CPU 時間・メモリ確保・malloc 回数・syscall 数・ARC の retain/release 回数など、豊富な組み込みメトリクスを計測できます。 - CI に組み込めば、プルリクエストごとに性能のリグレッション(劣化)を自動チェックできます。
mainブランチとの比較や、あらかじめ記録した絶対的なしきい値との比較が可能です。 - macOS と Linux の両方に対応し、ローカルでの手軽な性能検証から CI での自動チェックまでをカバーします。
何が発表されたのか
Benchmark は、Swift パッケージの性能を計測・検証するためのオープンソースパッケージです。「動くようにする・正しくする・速くする」という開発の指針のうち、最後の「速くする」を継続的に支えることを目的としています。
ユニットテストやインテグレーションテストが、機能が壊れたときにそれを検知してくれるのと同じように、ベンチマークを用意して継続的に実行すれば、性能が期待どおりでなくなったときにそれを検知できます。問題が見つかったら、Instruments・DTrace・Heaptrack・Leaks・Sample などの専用ツールで根本原因を分析して修正する、という流れになります。これは、ユニットテストの失敗をきっかけにデバッガや sanitizer で原因を突き止めるのと同じ構図です。
このパッケージは、マルチプラットフォーム対応・豊富なメトリクス・CI 連携・開発者にとっての扱いやすさという要件を満たす既存の解決策が Swift エコシステムに見当たらなかったため、専門的なトレーディングソフトウェアの開発で培われた知見をもとに開発され、オープンソース化されたものです。
何に使えるのか
Benchmark は SwiftPM のコマンドプラグインとして実装されており、次のコマンドでベンチマークを実行します。
swift package benchmark
ベンチマークは、計測したい処理を Benchmark のクロージャ内に書くだけで定義できます。たとえば Foundation.Date() の生成性能を計測する最小のベンチマークは次のようになります。
import Benchmark
import Foundation
let benchmarks = {
Benchmark("Foundation-Date") { benchmark in
for _ in benchmark.scaledIterations {
blackHole(Foundation.Date())
}
}
}
benchmark.scaledIterations を使って計測対象の処理を繰り返し、blackHole(_:) に結果を渡すことで、コンパイラの最適化によって計測対象が消されてしまうのを防ぎます。CPU 使用量を中心に見るマイクロベンチマークから、長時間動く複雑なベンチマークまで対応でき、HDR Histogram パッケージを利用して、長時間にわたる多数のサンプルを計測できます。
計測できるメトリクス
Benchmark は豊富な組み込みメトリクスを備えています。代表的なものを挙げます。
cpuUser/cpuSystem/cpuTotal— ユーザー空間・システム・合計の CPU 時間wallClock— 実時間(wall clock)throughput— 1 秒あたりの処理数(スループット)peakMemoryResident/peakMemoryVirtual— ピーク時の resident / virtual メモリ使用量mallocCountSmall/mallocCountLarge/mallocCountTotal— malloc の呼び出し回数(jemalloc 基準)allocatedResidentMemory— アプリケーションが確保した resident メモリ量memoryLeaked— メモリリークの可能性を示す値(malloc と free の差)syscalls/contextSwitches— syscall 数・コンテキストスイッチ数(macOS 限定)readSyscalls/writeSyscalls/readBytesLogicalなど — I/O 関連のメトリクス(Linux 限定)retainCount/releaseCount/retainReleaseDelta— ARC の retain / release 回数。retainReleaseDeltaが 0 でなければ retain cycle の存在を示唆します
このほか、キャッシュのヒット率などアプリケーション固有の値を計測するカスタムメトリクスもサポートされます。Linux・macOS それぞれの OS 固有メトリクスにも対応しており、サーバーサイド・デスクトップ・モバイルの各環境でリソース使用量を細かく把握できます。
メトリクスや実行回数のカスタマイズ
計測するメトリクスやスケーリングファクターは、Benchmark.Configuration で個別に指定できます。また、startMeasurement() と stopMeasurement() を使えば、セットアップのコストを計測対象から除外できます。
import Benchmark
import Foundation
import Histogram
let benchmarks = {
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() // ここまでのセットアップは計測しない
for i in benchmark.scaledIterations {
blackHole(histogram.valueAtPercentile(percentiles[i % percentiles.count]))
}
benchmark.stopMeasurement()
}
}
出力と分析
実行結果はデフォルトでは人間が読みやすい表形式で出力されます。これに加えて、他の可視化ツールでの分析に適したさまざまな出力フォーマットへのエクスポートにも対応しています。
サポートされる主なワークフロー
Benchmark は、次のような性能管理のワークフローを支援します。
- プルリクエストでの性能リグレッションの自動チェック — プルリクエストの性能メトリクスを
mainブランチと比較し、ベンチマークごとに指定した絶対値・相対値のしきい値を超える劣化があれば、ワークフローのチェックを失敗させます。 - あらかじめ記録した絶対的な p90 しきい値との比較 — 事前に決めたベースライン(たとえば malloc 回数)に対するチェックに向いています。
- 複数の性能ベースラインの手動比較 — 個々の開発者が反復的な改善や A/B 的な性能比較を行うのに使えます。
- 結果の各種フォーマットでのエクスポート — 分析や可視化に利用できます。
- Instruments プロファイラの実行 — ベンチマークの実行ファイルに対して、Xcode から直接 Instruments をかけられます。
導入・今後の位置づけ
ローカルでの手軽な性能検証から CI での自動チェックまでをカバーするため、個々の開発者は変更を push する前に素早く性能を確認でき、チームとしては性能のリグレッションを継続的に防げます。
発表時点で、Swift Foundation・SwiftPM・SwiftNIO・Google Flatbuffers といった主要なオープンソースプロジェクトが、性能最適化のために Benchmark パッケージを採用し始めていることが紹介されています。導入方法はドキュメントにまとめられており、Swift フォーラムの Benchmark カテゴリで議論できます。