Google Benchmarkを使用してC++コードをベンチマークする方法
Everett Pompeii
ベンチマークとは?
ベンチマークとは、コードの性能をテストして、その処理速度(レイテンシ)や処理量(スループット)を確認することを指します。 この、ソフトウェア開発において見落とされがちなステップは、高速で高性能なコードを作成および維持するために重要です。 ベンチマークは開発者がコードがさまざまな作業負荷や条件下でどれだけうまく動作するかを理解するために必要な指標を提供します。 機能のリグレッションを防ぐためにユニットテストや統合テストを書くのと同様に、 パフォーマンスのリグレッションを防ぐためにもベンチマークを書くべきです。 パフォーマンスのバグもバグです!
C++でFizzBuzzを書く
ベンチマークを書くためには、ベンチマークするソースコードが必要です。 まず、非常にシンプルなプログラム、 FizzBuzz を書いてみましょう。
FizzBuzzのルールは以下の通りです:
1
から100
までの整数を印刷するプログラムを書く:
- 三の倍数の場合は、
Fizz
を印刷します- 五の倍数の場合は、
Buzz
を印刷します- 三と五の両方の倍数の場合は、
FizzBuzz
を印刷します- それ以外の場合は、数字を印刷します
FizzBuzzは多くの方法で書くことができます。 なので、私のお気に入りの方法で進めることにしましょう。
1
から100
まで繰り返し、各繰り返し後にインクリメントします。- 各数値について、剰余(割り算の余り)を計算します。
- 余りが
0
の場合、その数は指定された因数の倍数です:15
の余りが0
の場合は、FizzBuzz
を出力します。3
の余りが0
の場合は、Fizz
を出力します。5
の余りが0
の場合は、Buzz
を出力します。
- それ以外の場合は、その数をそのまま出力します。
手順を追って従う
このステップバイステップのチュートリアルに従うためには、git
をインストールし、cmake
をインストールし、またGNUコンパイラコレクション(GCC)のg++
をインストールする必要があります。
🐰 この投稿のソースコードはGitHubで入手可能です。
game.cpp
という名前のC++ファイルを作成し、その内容を上述のFizzBuzz実装に設定します。
g++
を使用してgame
という名前の実行可能ファイルをビルドし、それを実行します。
出力は以下のようになるはずです:
🐰 よっしゃ!コーディング面接を突破していますね!
さらに進む前に、マイクロベンチマークとマクロベンチマークの違いを議論することが重要です。
マイクロベンチマークとマクロベンチマーク
ソフトウェアベンチマークには、マイクロベンチマークとマクロベンチマークの2つの主要なカテゴリーがあります。
マイクロベンチマークは、ユニットテストと同様のレベルで動作します。
例えば、単一の数値に対してFizz
、Buzz
、またはFizzBuzz
を決定する関数のベンチマークはマイクロベンチマークになります。
一方、マクロベンチマークは、統合テストと同様のレベルで動作します。
例えば、1
から100
までの全ゲームをプレイするFizzBuzzの関数のベンチマークは、マクロベンチマークになります。
一般的に、可能な限り低い抽象レベルでテストすることが最善です。 ベンチマークの場合、これにより保守性が向上し、測定値のノイズを減らすことに役立ちます。 しかし、エンドツーエンドテストがシステム全体が予想通りに組み合わさるかのサニチェックに非常に役立つように、 マクロベンチマークがあると、ソフトウェアを通る重要なパスが性能を維持するために非常に役立ちます。
C++におけるベンチマーク
C++でのベンチマークの一般的な選択肢は以下の2つです: Google Benchmark と Catch2。
Google Benchmarkは、C++用の堅牢で多用途なベンチマークライブラリであり、開発者が自分のコードの性能を高精度で測定できるようにします。その主要な利点の一つは、既存のプロジェクト、特にすでにGoogleTestを使用しているプロジェクトへの統合が容易であることです。Google Benchmarkは、CPU時間、実時間、メモリ使用量の測定能力を含む詳細な性能指標を提供します。簡単な関数ベンチマークから複雑なパラメータ化テストまで、広範囲のベンチマークシナリオをサポートしています。
Catch2は、C++用のモダンなヘッダーオンリーのテストフレームワークであり、テストの記述と実行のプロセスを簡素化します。その主要な利点の一つは、直感的で表現力豊かな構文であり、開発者が迅速かつ明確にテストを書くことができることです。Catch2は、単体テスト、統合テスト、振る舞い駆動開発(BDD)スタイルのテスト、および基本的なマイクロベンチマーク機能を含む広範囲のテストタイプをサポートしています。
両方ともBencherによってサポートされています。 では、なぜGoogle Benchmarkを選ぶのか? Google BenchmarkはGoogleTestとシームレスに統合され、 これはC++エコシステムで事実上の標準的なユニットテストハーネスです。 すでにGoogleTestを使用している場合、コードのレイテンシをベンチマークするにはGoogle Benchmarkを使用することをお勧めします。 つまり、Google Benchmarkは経過時間を計測するのに優れています。
FizzBuzzのリファクタリング
FizzBuzzアプリケーションをテストするためには、ロジックをプログラムのmain
関数から分離する必要があります。ベンチマークハーネスはmain
関数をベンチマークできません。これを実現するために、いくつかの変更を行う必要があります。
FizzBuzzのロジックを、新しいファイルplay_game.cpp
内のいくつかの関数にリファクタリングしましょう:
fizz_buzz
: 整数n
を受け取り、Fizz
、Buzz
、FizzBuzz
、または数値ロジックを実行して結果を文字列として返します。play_game
: 整数n
を受け取り、その数を使ってfizz_buzz
を呼び出し、should_print
がtrue
の場合は結果を出力します。
次に、play_game.h
という名前のヘッダーファイルを作成し、そこにplay_game
関数の宣言を追加します:
次に、main
関数をgame.cpp
で更新し、ヘッダーファイルからplay_game
関数定義を使用します:
プログラムのmain
関数は、1から100までの数値を反復処理し、各数値に対してplay_game
を呼び出し、should_print
をtrue
に設定します。
FizzBuzzのベンチマーク
コードをベンチマークするために、まずGoogle Benchmarkをインストールする必要があります。
ライブラリをクローンします。
クローンしたばかりのディレクトリに入ります。
cmake
を使って、ビルド出力を配置するためのビルドディレクトリを作成します。
ビルドシステムファイルを生成し、必要な依存関係をダウンロードするためにcmake
を使用します。
最後に、ライブラリをビルドします。
親ディレクトリに戻ります。
次に、新しいファイルbenchmark_game.cpp
を作成します。
play_game.h
から関数定義をインポートします。- Google
benchmark
ライブラリのヘッダをインポートします。 benchmark::State
への参照を受け取るBENCHMARK_game
関数を作成します。benchmark::State
オブジェクトを反復処理します。- 各イテレーションごとに、
1
から100
までを含めて反復処理します。- 現在の数値と
should_print
をfalse
に設定してplay_game
を呼び出します。
- 現在の数値と
BENCHMARK_game
関数をBENCHMARK
ランナーに渡します。BENCHMARK_MAIN
を使用してベンチマークを実行します。
これでコードをベンチマークする準備が整いました。
🐰 レタスが跳ねる!初めてのベンチマークメトリクスを取得しました!
最後に、我々の疲れた開発者たちの頭を休めることができます… 冗談です、ユーザーは新しい機能を求めています!
C++でFizzBuzzFibonacciを作成する
私たちの主要業績評価指標(KPI)が低下しているので、プロダクトマネージャー(PM)が新機能を追加したいと考えています。多くのブレインストーミングと多数のユーザーインタビューの結果、古き良きFizzBuzzでは不十分であると決定されました。最近の子供たちは新しいゲーム、FizzBuzzFibonacciを求めています。
FizzBuzzFibonacciの規則は以下の通りです:
1
から100
までの整数を印刷するプログラムを書く:
- 3の倍数には
Fizz
を印刷- 5の倍数には
Buzz
を印刷- 三と五の倍数には
FizzBuzz
を印刷- フィボナッチ数列の一部である数には、
Fibonacci
だけを印刷- それ以外のすべてには、その数値を印刷
フィボナッチ数列は、それぞれの数が前の二つの数の和である数列です。
例えば、0
と1
から始めると、フィボナッチ数列の次の数は1
になります。
そして、それに続く数は:2
, 3
, 5
, 8
と続きます。
フィボナッチ数列の一部である数はフィボナッチ数として知られています。なので、フィボナッチ数を検出する関数を書く必要があります。
フィボナッチ数列を書く方法はたくさんありますし、同様にフィボナッチ数を検出する方法もたくさんあります。 だから私のお気に入りを選びます:
- 整数を受け取り、真偽値を返す関数
is_fibonacci_number
を作成します。 - すべての数値
0
から与えられた数値n
まで(含む)を反復します。 - フィボナッチ数列を
0
と1
をprevious
とcurrent
の数として始めます。 current
の数が現在の反復i
より小さい間、繰り返します。previous
とcurrent
の数を加算してnext
の数を得ます。previous
の数をcurrent
の数に更新します。current
の数をnext
の数に更新します。current
が指定された数n
以上になったら、ループを抜けます。current
の数が指定された数n
と等しいか確認し、もしそうならtrue
を返します。- そうでない場合は、
false
を返します。
次に、fizz_buzz
関数を更新する必要があります:
fizz_buzz
関数をよりわかりやすくするためにfizz_buzz_fibonacci
に名前を変更します。is_fibonacci_number
のヘルパー関数を呼び出します。is_fibonacci_number
の結果がtrue
であれば、Fibonacci
を返します。is_fibonacci_number
の結果がfalse
であれば、同様にFizz
、Buzz
、FizzBuzz
、または数値ロジックを実行しその結果を返します。
fizz_buzz
を fizz_buzz_fibonacci
に名前を変更したため、play_game
関数を更新する必要もあります。
私たちの main
関数と BENCHMARK_game
関数はそのままで問題ありません。
フィズバズフィボナッチのベンチマーク
ベンチマークを再度実行できます:
ターミナルの履歴を戻ってスクロールすると、フィズバズゲームとフィズバズフィボナッチゲームのパフォーマンスをざっと比較できます: 1698 ns
vs 56190 ns
。
あなたの数値は私のとは少し異なるでしょう。
しかし、両ゲームの違いはおそらく50倍程度でしょう。
それは素晴らしいことです!特に、ゲームに派手な響きのある フィボナッチ という機能を追加するのに。
子供たちはそれを気に入るでしょう!
C++ での FizzBuzzFibonacci の拡張
私たちのゲームは大ヒットしています!子供たちは本当に FizzBuzzFibonacci を楽しんでいます。
そのため、経営陣から続編を求められるほどです。
しかし、これは現代の世界です。我々は1回きりの購入ではなく、毎年の定期収益(ARR)が必要です!
我々の新しいビジョンは、ゲームがオープンエンドであることです。1
と 100
の間の制約にとらわれることはありません(たとえそれが包括的であっても)。
いいえ、我々は新たなフロンティアへと進んでいます!
Open World FizzBuzzFibonacciのルールは次のとおりです:
以下のように、任意の 正の整数を受け取って印刷するプログラムを書きます:
- 3の倍数の場合、
Fizz
を出力する- 5の倍数の場合、
Buzz
を出力する- 3と5の両方の倍数の場合、
FizzBuzz
を出力する- フィボナッチ数列の一部である数値は、
Fibonacci
のみを出力する- それ以外の場合は、数値を出力する
ゲームをどの数字でもプレイできるようにするためには、コマンドライン引数を受け入れなければなりません。
main
関数を以下のように更新します:
argc
とargv
を取るようにmain
関数を更新します。- ゲームに渡された最初の引数を取得し、それが数字であるかどうかを確認します。
- もしそうであれば、最初の引数を整数
i
としてパースします。 - 新しくパースした整数
i
でゲームをプレイします。
- もしそうであれば、最初の引数を整数
- パースに失敗するか、引数が渡されていない場合、デフォルトで有効な入力を促します。
これで、どの数字でもゲームをプレイできるようになります!
game
実行ファイルを再コンパイルし、ゲームをプレイするために整数を続けて実行します:
無効な数字を省略または提供した場合:
わあ、これはかなり徹底したテストでした!CI が通過しました。上司たちは大喜びです。 それではリリースしましょう!🚀
終わり
🐰 … あなたのキャリアの終わりかもしれない?
冗談じゃない!全てが炎上しています!🔥
最初は全てうまく行っているように見えました。 しかし、土曜日の午前2時07分に僕のページャーが鳴った:
📟 あなたのゲームが炎上しています!🔥
ベッドから飛び起き、何が起こっているのかを理解しようとしました。 ログを検索しようと試みましたが、何もかもがクラッシュし続けていて困難でした。 最終的に、問題を見つけました。その子供たち! 彼らは私たちのゲームが大好きで、最大百万までプレイしていました! ひらめきの一瞬で、新たに2つのベンチマークを追加しました:
- 数字100(
100
)でゲームをプレイするためのマイクロベンチマークBENCHMARK_game_100
- 数字1,000,000(
1_000_000
)でゲームをプレイするためのマイクロベンチマークBENCHMARK_game_1_000_000
私が実行したとき、次のようになりました:
待ってください…待ってください…
なんと! 1,249 ns
x 10,000
は12,490,000 ns
のはずなのに110,879,642 ns
🤯
フィボナッチ数列のコードは機能的に正しいはずですが、どこかにパフォーマンスの問題があります。
C++でのFizzBuzzFibonacciを修正
is_fibonacci_number
関数をもう一度見てみましょう:
パフォーマンスを考えてみると、不必要なループがひとつあることに気付きます。
for (int i = 0; i <= n; ++i)
ループは完全に取り除いて、
current
の値を指定された数(n
)と比較するだけで済みます 🤦
is_fibonacci_number
関数を更新します。0
と1
をそれぞれprevious
とcurrent
の数字としてフィボナッチ数列を初期化します。current
の数が指定された数n
より小さい間、繰り返し処理を行います。previous
とcurrent
の数を足してnext
の数を得ます。previous
の数をcurrent
の数に更新します。current
の数をnext
の数に更新します。current
が指定された数n
以上になったら、ループを終了します。current
の数が指定された数n
と等しいかどうかを確認し、その結果を返します。
それでは、もう一度これらのベンチマークを実行し、結果を見てみましょう:
おお、なんと! BENCHMARK_game
ベンチマークは元のFizzBuzzに戻ってきましたね。
そのスコアが正確にはどのくらいだったのか思い出せませんが。
3週間も前のことで、私のターミナル履歴はそんなに遡れず、Google Benchmarkは結果を保存しません。
でも、近いと思います!
BENCHMARK_game_100
ベンチマークは約50倍に減少して34.4 ns
です。
そして、BENCHMARK_game_1_000_000
ベンチマークは1,500,000倍以上の削減! 110,879,642 ns
から61.6 ns
に!
🐰 まあ、少なくともこの性能問題をプロダクションに持ち込む前に見つけたのは良かったですね…あ、そうだね。冗談はさておき…
CIでパフォーマンスの後退を捕捉する
私のちょっとしたパフォーマンスのバグが原因で我々のゲームが大量の否定的なレビューを受けたことに、エクゼクティブたちは不満を持っていました。 彼らは再びそれを起こさないようにと言い、どうすれば良いのか尋ねると、ただ再びやらないようにと言われるだけでした。 どうすればそれを管理することができるのでしょうか‽
幸運なことに、Bencherという素晴らしいオープンソースツールを見つけました。 超大盤振る舞いの無料枠があるので、私の個人的なプロジェクトではBencherクラウドをただ使うことができます。 そして、仕事では全てが私たちのプライベートクラウド内にある必要があるので、Bencher Self-Hostedを使い始めました。
Bencherには組み込みのアダプターがあり、 そのためCIに簡単に統合することができます。クイックスタートガイドをフォローした後、 私は私のベンチマークを実行し、それらをBencherで追跡することができます。
この素敵なウサギが私にくれた便利なタイムマシンを使って、私たちがずっとBencherを使っていたらどうなっていたかを時間を遡って再現しました。 最初にバギーなFizzBuzzFibonacciの実装をプッシュしたところを見ることができます。 私のプルリクエストに対するコメントとしてCIで直ちに失敗が出ました。 その同じ日に、私はその無駄な、余分なループを取り除くことでパフォーマンスのバグを修正しました。 火事はありません。ただ幸せなユーザーたちだけです。
Bencher: 連続ベンチマーキング
Bencherは、連続ベンチマーキングツールのスイートです。 パフォーマンスの後退があなたのユーザーに影響を与えたことはありますか? Bencherなら、それが起こるのを防げた可能性があります。 Bencherは、パフォーマンスの低下を_productionに到達する_前に検出し、防止することを可能にします。
- 実行: お気に入りのベンチマーキングツールを使用してベンチマークをローカルまたはCIで実行します。
bencher
CLIは単にあなたの既存のベンチマークハーネスをラップし、その結果を保存します。 - 追跡: ベンチマークの結果を時間と共に追跡します。ソースブランチ、テストベッド、測定基準に基づいてBencherのWebコンソールを使用して結果を監視、クエリ、グラフ化します。
- キャッチ: CIでパフォーマンスの後退をキャッチします。Bencherは最先端のカスタマイズ可能な分析を使用して、パフォーマンスの後退がProductionに到達する前にそれを検出します。
機能の後退を防ぐためにユニットテストがCIで実行されるのと同じ理由で、Bencherを使用してCIでベンチマークを実行してパフォーマンスの後退を防ぐべきです。パフォーマンスのバグはバグです!
CIでパフォーマンスの回帰を捉えるのを開始してください - Bencher Cloudを無料で試す。