Rustでカスタムベンチマークハーネスを構築する方法
Everett Pompeii
ベンチマークとは?
ベンチマークとは、コードの性能をテストして、その処理速度(レイテンシ)や処理量(スループット)を確認することを指します。 この、ソフトウェア開発において見落とされがちなステップは、高速で高性能なコードを作成および維持するために重要です。 ベンチマークは開発者がコードがさまざまな作業負荷や条件下でどれだけうまく動作するかを理解するために必要な指標を提供します。 機能のリグレッションを防ぐためにユニットテストや統合テストを書くのと同様に、 パフォーマンスのリグレッションを防ぐためにもベンチマークを書くべきです。 パフォーマンスのバグもバグです!
Rustにおけるベンチマーク
Rustでのベンチマークにおいて人気のあるオプションは、libtest bench、Criterion、そしてIaiです。
libtestはRustの組み込みユニットテストとベンチマークフレームワークです。
Rust標準ライブラリの一部であるにもかかわらず、libtest benchはまだ不安定とされており、nightly
コンパイラリリースでのみ利用可能です。
安定バージョンのRustコンパイラで動作させるためには、別のベンチマークハーネスを使用する必要があります。
しかし、どちらも積極的に開発されているわけではありません。
Rustエコシステム内で最も人気のあるベンチマークハーネスはCriterionです。
これは安定版とnightly
版の両方のRustコンパイラリリースで動作し、Rustコミュニティ内で事実上の標準として定着しています。
Criterionはlibtest benchと比べてはるかに多機能です。
Criterionの実験的な代替としてIaiがありますが、これはCriterionの作成者によって開発されたものです。 しかし、壁時計時間の代わりに命令カウントを使用します:CPU命令、L1アクセス、L2アクセス、RAMアクセスです。 これにより、これらのメトリクスはラン間でほぼ同一であるべきなので、シングルショットベンチマークが可能になります。
以上のことを念頭に置いて、コードのウォールクロックタイムをベンチマークしたいのであれば、Criterionを使用することをお勧めします。 共有ランナーを使用してCIでコードをベンチマークしたい場合は、Iaiをチェックしてみる価値があります。 ただし、Iaiは3年以上更新されていません。 そのため、代わりにIai-Callgrindを使用することを検討するかもしれません。
しかし、ウォールクロックタイムや命令カウントをベンチマークしたくない場合はどうでしょうか? 全く異なるベンチマークを追跡したい場合はどうでしょうか? 幸運なことに、Rustを使用するとカスタムベンチマークハーネスを非常に簡単に作成できます。
cargo bench
の動作
カスタムベンチマークハーネスを作成する前に、Rust ベンチマークの仕組みを理解する必要があります。
ほとんどのRust開発者にとって、これは cargo bench
コマンド を実行することを意味します。
cargo bench
コマンドはベンチマークをコンパイルして実行します。
デフォルトでは、cargo bench
は組み込み(ただし不安定な)libtest ベンチハーネスを使用しようとします。
libtest ベンチはコードを通過して #[bench]
属性で注釈されたすべての関数を実行します。
カスタムベンチマークハーネスを使用するためには、cargo bench
に libtest ベンチを使用しないように指示する必要があります。
cargo bench
でカスタムベンチマークハーネスを使用する
cargo bench
にlibtestのベンチを使用させないようにするためには、Cargo.toml
ファイルに以下を追加する必要があります。
残念ながら、カスタムベンチマークハーネスでは#[bench]
属性を使用できません。
いつの日か使えるようになるかもしれませんが、今日はまだです。
その代わりに、ベンチマークを保持するための別のbenches
ディレクトリを作成する必要があります。
benches
ディレクトリは、統合テストにおける tests
ディレクトリ に相当します。
benches
ディレクトリ内の各ファイルは別々のクレートとして扱われます。
そのため、ベンチマークの対象となるクレートはライブラリクレートでなければなりません。
つまり、lib.rs
ファイルを持っている必要があります。
例えば、game
という基本的なライブラリクレートがあれば、benches
ディレクトリにplay_game
という名前のカスタムベンチマークファイルを追加することができます。
ディレクトリ構造は以下のようになります:
次に、cargo bench
にカスタムベンチマーククレートplay_game
について知らせる必要があります。
そのために、Cargo.toml
ファイルを更新します:
ベンチマークするコードを書く
パフォーマンステストを書く前に、ベンチマークするためのライブラリコードが必要です。 私たちの例では、FizzBuzzFibonacciゲームを行います。
FizzBuzzFibonacciのルールは以下の通りです:
1
から100
(含む)までの整数を出力するプログラムを書いてください:
- 3の倍数の場合、
Fizz
を出力- 5の倍数の場合、
Buzz
を出力- 3と5の両方の倍数の場合、
FizzBuzz
を出力- フィボナッチ数列の一部である数の場合、
Fibonacci
のみを出力- その他のすべての数については、その数を出力
src/lib.rs
の実装は以下の通りです:
カスタムベンチマークハーネスの作成
benches/play_game.rs
内にカスタムベンチマークハーネスを作成します。
このカスタムベンチマークハーネスはヒープの割り当てを測定するものです。
測定には the dhat-rs
crate を使用します。
dhat-rs
は Rust プログラムのヒープ割り当てを追跡するための素晴らしいツールで、
Rust のパフォーマンスエキスパートである Nicholas Nethercote によって作成されました。
ベンチマーク関数の管理を支援するために、
非常に多作な David Tolnay による the inventory
crate を使用します。
それでは、dhat-rs
と inventory
を Cargo.toml
ファイルに dev-dependencies
として追加しましょう:
カスタムアロケータの作成
独自のベンチマークハーネスではヒープ割り当てを測定するため、カスタムヒープアロケータを使用する必要があります。
Rustでは、 #[global_allocator]
属性 を使用してカスタムのグローバルヒープアロケータを設定できます。
benches/play_game.rs
の先頭に次のコードを追加します:
これにより、Rustは dhat::Alloc
をグローバルヒープアロケータとして使用することを指示します。
🐰 グローバルヒープアロケータを設定できるのは一つだけです。 複数のグローバルアロケータを切り替える場合は、Rustのフィーチャー を使用した条件付きコンパイルで管理する必要があります。
カスタムベンチマークコレクターの作成
カスタムベンチマークハーネスを作成するためには、ベンチマーク関数を識別して保存する方法が必要です。
CustomBenchmark
という構造体を使用して、各ベンチマーク関数をカプセル化します。
CustomBenchmark
は名前と、結果として dhat::HeapStats
を返すベンチマーク関数を持ちます。
次に、inventory
クレートを使用して、すべての CustomBenchmark
のコレクションを作成します:
ベンチマーク関数の作成
さて、FizzBuzzFibonacciゲームを実行するベンチマーク関数を作成しましょう:
一行ずつ確認していきます:
CustomBenchmark
で使用された署名に一致するベンチマーク関数を作成します。- テストモードで
dhat::Profiler
を作成し、dhat::Alloc
のカスタムグローバルアロケーターから結果を収集します。 - コンパイラがコードを最適化しないように、「ブラックボックス」内で
play_game
関数を実行します。 1
から100
まで反復します(包含)。- 各数値に対して、
print
をfalse
に設定してplay_game
を呼び出します。 - ヒープ割り当て統計を
dhat::HeapStats
として返します。
🐰
play_game
関数のfalse
に設定しました。 これにより、play_game
が標準出力に出力しないようにします。 このようにライブラリ関数をパラメーター化すると、 ベンチマークを行いやすくなります。 しかし、これはライブラリを実際に使用する際と 全く同じ方法でベンチマークを行わないことになります。この場合、自分たちに問う必要があります:
- 標準出力に出力するためのリソースを気にする必要があるか?
- 標準出力に出力することがノイズの原因となる可能性があるか?
この例では、次のように決定しました:
- いいえ、標準出力に出力することは気にしません。
- はい、大変なノイズの原因となる可能性があります。
したがって、このベンチマークの一部として標準出力への出力を省略しました。 ベンチマークは難しく、このような質問に対する唯一の正解はありません。 状況次第です.
ベンチマーク関数の登録
ベンチマーク関数を書いたら、それを CustomBenchmark
として作成し、inventory
を使用してベンチマークコレクションに登録する必要があります。
もし複数のベンチマークがあれば、このプロセスを繰り返します:
- ベンチマーク関数を作成する。
- ベンチマーク関数のための
CustomBenchmark
を作成する。 CustomBenchmark
をinventory
コレクションに登録する。
カスタムベンチマークランナーの作成
最後に、カスタムベンチマークハーネスのためのランナーを作成する必要があります。 カスタムベンチマークハーネスは、実際にはベンチマークをすべて実行し、結果を報告するバイナリです。 ベンチマークランナーはそのすべてを統括します。
結果をBencher Metric Format (BMF) JSONで出力したいと思います。
これを実現するために、最後の依存関係として
David Tolnayによるserde_json
クレートを追加します!
次に、CustomBenchmark
がベンチマーク関数を実行し、その結果をBMF JSONとして返すメソッドを実装します。
BMF JSON結果には、各ベンチマークに対して6つのメトリクスが含まれます:
- Final Blocks: ベンチマーク終了時に割り当てられた最終的なブロック数。
- Final Bytes: ベンチマーク終了時に割り当てられた最終的なバイト数。
- Max Blocks: ベンチマーク実行中に一度に割り当てられた最大ブロック数。
- Max Bytes: ベンチマーク実行中に一度に割り当てられた最大バイト数。
- Total Blocks: ベンチマーク実行中に割り当てられた総ブロック数。
- Total Bytes: ベンチマーク実行中に割り当てられた総バイト数。
最後に、inventory
コレクション内のすべてのベンチマークを実行し、
その結果をBMF JSONとして出力するmain
関数を作成できます。
カスタムベンチマークハーネスを実行する
すべてが整いました。 ついにカスタムベンチマークハーネスを実行することができます。
標準出力およびresults.json
という名前のファイルに出力される結果は次のようになります:
表示される正確な数値は、コンピューターのアーキテクチャによって少し異なる場合があります。 しかし、重要なのは少なくとも最後の4つの指標にいくつかの値があることです。
カスタムベンチマーク結果を追跡する
ほとんどのベンチマーク結果は一時的なものです。 ターミナルのスクロールバック制限に達すると消えてしまいます。 一部のベンチマークハーネスでは結果をキャッシュできますが、それを実装するのは非常に手間がかかります。 さらに、その結果をローカルに保存することしかできません。 幸運なことに、私たちのカスタムベンチマークハーネスはBencherと連携します! Bencherは継続的ベンチマークツールのスイートであり、 ベンチマークの結果を時間をかけて追跡し、 パフォーマンスの退行を本番環境に到達する_前に_捕捉することができます。
Bencher CloudまたはBencher Self-Hostedのセットアップが完了すると、 次のコマンドを実行して、カスタムベンチマークハーネスからの結果を追跡できます:
また、カスタムベンチマークをBencherで追跡する方法に関する詳細や、 JSONベンチマークアダプターについても読むことができます。
まとめ
この投稿では、Rustエコシステムで最も人気のある3つのベンチマークハーネスについて紹介しました:libtest bench、Criterion、およびIaiです。これらは大部分のユースケースをカバーしますが、場合によってはウォールクロックタイムや命令カウント以外のものを測定する必要があるかもしれません。そのために、カスタムベンチマークハーネスを作成することにしました。
私たちのカスタムベンチマークハーネスは、dhat-rs
を使用してヒープアロケーションを測定します。ベンチマーク関数は inventory
を使用して収集されます。実行すると、ベンチマーク結果は Bencher Metric Format (BMF) JSON として出力されます。そして、Bencherを使用してカスタムベンチマーク結果を時間経過とともに追跡し、CIでパフォーマンスのリグレッションを検出することができます。
このガイドのすべてのソースコードはGitHubで利用可能です。
Bencher: 連続ベンチマーキング
Bencherは、連続ベンチマーキングツールのスイートです。 パフォーマンスの後退があなたのユーザーに影響を与えたことはありますか? Bencherなら、それが起こるのを防げた可能性があります。 Bencherは、パフォーマンスの低下を_productionに到達する_前に検出し、防止することを可能にします。
- 実行: お気に入りのベンチマーキングツールを使用してベンチマークをローカルまたはCIで実行します。
bencher
CLIは単にあなたの既存のベンチマークハーネスをラップし、その結果を保存します。 - 追跡: ベンチマークの結果を時間と共に追跡します。ソースブランチ、テストベッド、測定基準に基づいてBencherのWebコンソールを使用して結果を監視、クエリ、グラフ化します。
- キャッチ: CIでパフォーマンスの後退をキャッチします。Bencherは最先端のカスタマイズ可能な分析を使用して、パフォーマンスの後退がProductionに到達する前にそれを検出します。
機能の後退を防ぐためにユニットテストがCIで実行されるのと同じ理由で、Bencherを使用してCIでベンチマークを実行してパフォーマンスの後退を防ぐべきです。パフォーマンスのバグはバグです!
CIでパフォーマンスの回帰を捉えるのを開始してください - Bencher Cloudを無料で試す。