pytest-benchmarkを使用してPythonコードをベンチマークする方法
Everett Pompeii
ベンチマークとは?
ベンチマークとは、コードの性能をテストして、その処理速度(レイテンシ)や処理量(スループット)を確認することを指します。 この、ソフトウェア開発において見落とされがちなステップは、高速で高性能なコードを作成および維持するために重要です。 ベンチマークは開発者がコードがさまざまな作業負荷や条件下でどれだけうまく動作するかを理解するために必要な指標を提供します。 機能のリグレッションを防ぐためにユニットテストや統合テストを書くのと同様に、 パフォーマンスのリグレッションを防ぐためにもベンチマークを書くべきです。 パフォーマンスのバグもバグです!
PythonでFizzBuzzを書く
In order to write benchmarks, we need some source code to benchmark. To start off we are going to write a very simple program, FizzBuzz.
The rules for FizzBuzz are as follows:
Write a program that prints the integers from
1
to100
(inclusive):
- For multiples of three, print
Fizz
- For multiples of five, print
Buzz
- For multiples of both three and five, print
FizzBuzz
- For all others, print the number
There are many ways to write FizzBuzz. So we’ll go with the my favorite:
1
から100
まで繰り返し、範囲を101
に設定します。- 各数について、
3
と5
の剰余(除算後の余り)を計算します。 - 余りが
0
の場合、その数は指定された因数の倍数です。- 余りが
0
で15
の場合、FizzBuzz
と表示します。 - 余りが
0
で3
の場合、Fizz
と表示します。 - 余りが
0
で5
の場合、Buzz
と表示します。
- 余りが
- それ以外の場合は、その数をそのまま表示します。
手順を順を追ってフォロー
この手順ごとのチュートリアルをフォローするには、Pythonをインストールし、 pipenv
をインストールする必要があります。
🐰 この投稿のソースコードはGitHubで利用可能です。
game.py
という名前のPythonファイルを作成し、その内容を上記のFizzBuzz実装に設定します。
次に、python game.py
を実行します。
出力は次のようになります:
🐰 バム!コーディングインタビューを突破していますね!
さらに進む前に、マイクロベンチマークとマクロベンチマークの違いについて話し合うことが重要です。
マイクロベンチマークとマクロベンチマーク
ソフトウェアベンチマークには、マイクロベンチマークとマクロベンチマークの2つの主要なカテゴリーがあります。
マイクロベンチマークは、ユニットテストと同様のレベルで動作します。
例えば、単一の数値に対してFizz
、Buzz
、またはFizzBuzz
を決定する関数のベンチマークはマイクロベンチマークになります。
一方、マクロベンチマークは、統合テストと同様のレベルで動作します。
例えば、1
から100
までの全ゲームをプレイするFizzBuzzの関数のベンチマークは、マクロベンチマークになります。
一般的に、可能な限り低い抽象レベルでテストすることが最善です。 ベンチマークの場合、これにより保守性が向上し、測定値のノイズを減らすことに役立ちます。 しかし、エンドツーエンドテストがシステム全体が予想通りに組み合わさるかのサニチェックに非常に役立つように、 マクロベンチマークがあると、ソフトウェアを通る重要なパスが性能を維持するために非常に役立ちます。
Python におけるベンチマーク
Python でのベンチマークの一般的な選択肢は次の2つです: pytest-benchmark および airspeed velocity (asv)
pytest-benchmark
は、人気のあるpytest
テストフレームワークと統合された強力なベンチマークツールです。
開発者は、ユニットテストと並行してベンチマークを実行することで、自分のコードのパフォーマンスを測定し比較することができます。
ユーザーは、ローカルでベンチマーク結果を簡単に比較でき、JSON などの様々な形式で結果をエクスポートすることも可能です。
airspeed velocity (asv)
は、Python エコシステム内のもう1つの高度なベンチマークツールです。
asv
の主な利点の1つは、詳細でインタラクティブな HTML レポートを生成する機能であり、パフォーマンストレンドを視覚化し、リグレッションを特定するのが容易になります。
さらに、asv
は相対的継続的ベンチマークを標準でサポートしています。
両方ともBencherによるサポートがあります。 では、なぜpytest-benchmark
を選ぶのでしょうか? pytest-benchmark
は、Pythonエコシステムにおけるデファクトスタンダードなユニットテストハーネスであるpytest
とシームレスに統合されます。 特にすでにpytest
を使用している場合は、コードのレイテンシをベンチマークするためにpytest-benchmark
を使用することをお勧めします。つまり、pytest-benchmark
はウォールクロック時間を測定するのに優れています。
フィズバズのリファクタリング
フィズバズアプリケーションをテストするためには、 ロジックをプログラムのメイン実行から切り離す必要があります。 ベンチマークハーネスはメイン実行をベンチマークすることができません。 これを行うために、いくつかの変更を行う必要があります。
フィズバズのロジックをいくつかの関数にリファクタリングしましょう:
play_game
: 整数n
を受け取り、その数でfizz_buzz
を呼び出し、should_print
がTrue
の場合は結果を出力します。fizz_buzz
: 整数n
を受け取り、実際のFizz
、Buzz
、FizzBuzz
、または数のロジックを実行し、結果を文字列として返します。
次に、メインの実行を次のように更新します:
プログラムのメイン実行は、1
から 100
までの数を含む範囲を繰り返し、各数に対して play_game
を should_print
を True
に設定して呼び出します。
FizzBuzzのベンチマーク
コードのベンチマークを行うためには、ベンチマークを実行するテスト関数を作成する必要があります。game.py
の最後に次のコードを追加します:
pytest-benchmark
のbenchmark
フィクスチャを取るtest_game
という名前の関数を作成します。1
から100
までを含めて反復するrun_game
関数を作成します。- 各数字に対して、
should_print
をFalse
に設定してplay_game
を呼び出します。
- 各数字に対して、
benchmark
ランナーにrun_game
関数を渡します。
次に、プロジェクトを構成してベンチマークを実行できるようにしましょう。
pipenv
で新しい仮想環境を作成します:
その新しいpipenv
環境内にpytest-benchmark
をインストールします:
これでコードのベンチマークを実行する準備が整いました。pytest game.py
を実行します:
🐰 レタスをターンアップします!最初のベンチマークメトリックが得られました!
最後に、疲れた開発者の頭を休めることができます… 冗談です、ユーザーは新しい機能を望んでいます!
PythonでFizzBuzzFibonacciを書く
私たちの主要業績評価指標(KPI)が減少しているため、プロダクトマネージャー(PM)は新しい機能を追加するよう求めています。 多くのブレインストーミングと多数のユーザーインタビューの結果、 昔ながらのFizzBuzzだけでは十分ではないと決まりました。 今どきの子供たちは新しいゲーム、FizzBuzzFibonacciを望んでいます。
The rules for FizzBuzzFibonacci are as follows:
Write a program that prints the integers from
1
to100
(inclusive):
- For multiples of three, print
Fizz
- For multiples of five, print
Buzz
- For multiples of both three and five, print
FizzBuzz
- For numbers that are part of the Fibonacci sequence, only print
Fibonacci
- For all others, print the number
The Fibonacci sequence is a sequence in which each number is the sum of the two preceding numbers.
For example, starting at 0
and 1
the next number in the Fibonacci sequence would be 1
.
Followed by: 2
, 3
, 5
, 8
and so on.
Numbers that are part of the Fibonacci sequence are known as Fibonacci numbers. So we’re going to have to write a function that detects Fibonacci numbers.
There are many ways to write the Fibonacci sequence and likewise many ways to detect a Fibonacci number. So we’ll go with the my favorite:
- 整数を受け取り、ブール値を返す
is_fibonacci_number
という関数を作成します。 - すべての数値を
0
から与えられた数値n
まで含めて反復します。 - フィボナッチ数列を
0
と1
から開始し、それぞれprevious
とcurrent
の数字として初期化します。 current
の数が現在のイテレーションi
より小さい間反復します。previous
とcurrent
の数字を加えてnext_value
の数字を得ます。previous
の数字をcurrent
の数字に更新します。current
の数字をnext_value
の数字に更新します。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
関数も更新する必要があります:
メインの実行とtest_game
関数はまったく同じままです。
FizzBuzzFibonacciのベンチマーク
これでベンチマークを再度実行することができます:
ターミナル履歴をスクロールして、
FizzBuzzとFizzBuzzFibonacciゲームのパフォーマンスを目視で比較してみましょう: 10.8307 us
vs 735.5682 us
。
あなたの数値は私のとは少し異なるでしょう。
しかし、2つのゲームの差はおそらく50倍の範囲になると思われます。
私にはこれが良いと思います! 特に、ゲームに_Fibonacci_という響きの良い機能を追加する際には。
子供たちはそれを気に入るでしょう!
PythonでのFizzBuzzFibonacciの拡張
我々のゲームは大ヒットです!子供たちは本当にFizzBuzzFibonacciを楽しんでいます。
その人気ぶりから、経営陣からの要望で続編を求められています。
しかし、これは現代の世界です。我々は一度の購入ではなく、年間繰り返し収入(ARR)が必要です!
我々のゲームの新しいビジョンは、境界のないオープンエンド形式にすることです。1
から100
までの範囲内に収まっていてはいけません(たとえそれが包括的であっても)。
いいえ、私たちは新しいフロンティアへと進んでいます!
The rules for Open World FizzBuzzFibonacci are as follows:
Write a program that takes in any positive integer and prints:
- For multiples of three, print
Fizz
- For multiples of five, print
Buzz
- For multiples of both three and five, print
FizzBuzz
- For numbers that are part of the Fibonacci sequence, only print
Fibonacci
- For all others, print the number
ゲームをどのような数でも動作させるために、コマンドライン引数を受け入れる必要があります。 メイン実行を次のように更新してください:
sys
パッケージをインポートします。- コマンドラインからゲームに渡されたすべての引数(
args
)を収集します。 - ゲームに渡された最初の引数を取得し、それが数字かどうかを確認します。
- もしそうなら、最初の引数を整数としてパースし、
i
とします。 - 新しくパースした整数
i
を使ってゲームをプレイします。
- もしそうなら、最初の引数を整数としてパースし、
- パースに失敗するか、または引数が渡されない場合は、有効な入力を促すデフォルトに戻ります。
これで、任意の数でゲームをプレイできます!
ゲームをプレイするには、python game.py
の後に整数を入力してください:
無視するか無効な数を提供した場合:
素晴らしい、徹底的なテストでした!CIが通過しました。上司たちは大喜びです。 出荷しましょう!🚀
終わり
🐰 … あなたのキャリアの終わりかもしれない?
冗談じゃない!全てが炎上しています!🔥
最初は全てうまく行っているように見えました。 しかし、土曜日の午前2時07分に僕のページャーが鳴った:
📟 あなたのゲームが炎上しています!🔥
ベッドから飛び起き、何が起こっているのかを理解しようとしました。 ログを検索しようと試みましたが、何もかもがクラッシュし続けていて困難でした。 最終的に、問題を見つけました。その子供たち! 彼らは私たちのゲームが大好きで、最大百万までプレイしていました! ひらめきの一瞬で、新たに2つのベンチマークを追加しました:
- 数字の百(
100
)でゲームをするためのマイクロベンチマークtest_game_100
- 数字の百万(
1_000_000
)でゲームをするためのマイクロベンチマークtest_game_1_000_000
実行してみたところ、次のような結果が得られました:
しばらくお待ちください…
何ですと! 15.8470 us
x 1,000
は 15,847.0 us
になるべきところが、 571,684.6334 us
ではないか 🤯
Fibonacciシーケンスのコードが機能的には正しいのに、どこかにパフォーマンス上のバグがあるに違いない。
PythonでのFizzBuzzFibonacciの修正
is_fibonacci_number
関数をもう一度見てみましょう:
パフォーマンスを考慮するときに、不要で余分なループがあることに気付きました。
for i in range(n + 1):
ループを完全になくし、
current
の値を与えられた数 (n
) と比較するだけでよいのです 🤦
is_fibonacci_number
関数を更新します。- フィボナッチ数列を
0
と1
で始め、それぞれをprevious
とcurrent
の数字として初期化します。 current
の数字が_与えられた数_n
より小さい間、繰り返します。previous
とcurrent
の数字を加えてnext_value
の数字を得ます。previous
の数字をcurrent
の数字に更新します。current
の数字をnext_value
の数字に更新します。current
が与えられた数n
以上になったら、ループを終了します。current
の数字が与えられた数n
と等しいか確認し、その結果を返します。
さて、それらのベンチマークを再実行して、どうなったか見てみましょう:
おお、すごい! test_game
のベンチマークが元のFizzBuzzレベルに戻りました。 具体的にどのスコアだったのか正確には思い出せませんが、3週間も経っていて、ターミナルの履歴もそこまで遡れません。 そして、pytest-benchmark
は要求した時にのみ結果を保存しますが、おそらく近いと思います!
test_game_100
のベンチマークはほぼ50倍に減少し、322.0815 ns
になりました。
そしてtest_game_1_000_000
のベンチマークはなんと500,000倍以上も減少しました! 571,684,633.4 ns
から753.1445 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を無料で試す。