cProfile を使った Python のプロファイリング: 具体的な事例

プログラムを書くのは楽しいですが、それを高速にするのは面倒なことです。 Python プログラムもその例外ではありませんが、基本的なプロファイリングのツールチェーンは、実はそれほど複雑な使い方をしているわけではありません。 ここでは、Python コードをすばやくプロファイルおよび解析して、コードのどの部分を最適化すべきかを見つける方法を紹介したいと思います。

Python プログラムのプロファイル化は、プログラムとそれを構成するすべてのものの実行時間を測定する動的解析を行うことです。 これは、各関数で費やされた時間を測定することを意味します。 これは、プログラムがどこで時間を費やしているか、そして、どの領域が最適化する価値があるかについてのデータを提供します。 たとえば、Python の関数 rangexrange のどちらが速くなるかを決定するなど、多くの人が局所的な最適化に焦点を当てます。 そして、上記の関数のいずれかによって得られる時間は、それを調査したり、同僚とそれについて議論したりするのに費やす時間に見合わないかもしれません。 根性だけでは必ずしも十分ではありません。

測定できるものがたくさんあるように、プロファイリングにも多くの種類があります。 この演習では、CPU使用率プロファイリング、つまり各機能が命令を実行するのに費やした時間に焦点を当てます。 たとえば、各コードのピースで使用されるメモリを測定するメモリ プロファイリングなどです。 基本的な使い方は次のとおりです:

このモジュールでスクリプトを実行することもでき、これは便利です:

これは、呼び出したすべての関数、それぞれに費やした時間、呼び出した回数を出力します。 より高度な可視化のために、私は KCacheGrind を活用しています。 この数年、C プログラミングやプロファイリングを行っていれば、主に Valgrind が生成するコール グラフのフロントエンドとして設計されているため、使用したことがあるかもしれません。 そのために、pyprof2calltreeを使います。

$ python -m cProfile -o myscript.cprof myscript.py$ pyprof2calltree -k -i myscript.cprof

そして、KCacheGrindウィンドウが魔法のように現れます!

具体例です。 Carbonara の最適化

Gnocchi 用に書いた小さな時系列ライブラリ Carbonara の性能に興味がありました。 7654>

プログラムをプロファイリングするには、それを実行する必要があります。 しかし、プログラム全体をプロファイリングモードで実行すると、気にしない多くのデータを生成し、理解しようとしていることにノイズを加えてしまいます。 Gnocchi には何千ものユニット テストがあり、Carbonara 自体にもいくつかあるので、ライブラリの基本機能をよく反映するものとして、これらのユニット テストで使用されるコードをプロファイルすることにしました。

これは、好奇心が強く、単純なファーストパス プロファイリングの良い戦略だということに注意してください。 したがって、プログラムの最適化をさらに推し進める必要があり、認識可能で価値のある利益を得たい場合、実運用で見られるものを模倣した条件およびシナリオでのプロファイリングがしばしば必要です。

私は上記の方法を使用して cProfile を有効にし、私のテストの周りに cProfile.Profile オブジェクトを作成しました(私は実際に testtools でこれを実装しはじめました)。 その後、私は上記のように KCacheGrind を実行しました。 7654>

ここで私がプロファイルしたテストはtest_fetchと呼ばれ、かなりわかりやすく、timeserieオブジェクトにデータを入れて、集計結果を取得するものです。 上のリストでは、88 % のティックが set_values で費やされていることがわかります (50以上のティックは44個)。 この関数はtimeserieに値を挿入するために使用され、値をフェッチするために使用されているわけではありません。

リストの残りを読むと、update_first_block_timestamp_truncate_resampleなど、いくつかの関数が残りの目盛りを共有していることがわかります。 リストの中のいくつかの関数はCarbonaraの一部ではないので、それらを最適化しようとしても意味がありません。 最適化できるのは、場合によっては、呼び出される回数だけです。

呼び出しグラフから、ここで何が起こっているのか、もう少し理解できます。 Carbonaraがどのように動作するかについての私の知識を使うと、_first_block_timestampの左側のスタック全体はあまり意味をなさないと思います。 この関数は、集計の最初のタイムスタンプを見つけることになっています。たとえば、タイムスタンプが 13:34:45 で期間が 5 分の場合、この関数は 13:30:00 を返すはずです。 現在のところ、1つの要素しかないタイムスリリーに対してPandasのresample関数を呼び出すことで動作していますが、これは非常に遅いようです。 実際、現在この関数は set_values が費やす時間の 25% (44 分の 11 tick) を占めています。

幸い、私は最近 _round_timestamp という小さな関数を追加しましたが、これは _first_block_timestamp が必要とすることを Pandas 関数を呼ばずに正確に行うので resample はいません。

そして、全く同じテストを再実行して cProfile の出力を比較しました。

今回の関数リストはかなり違うようです。 set_values が使用した時間数は 88 % から 71 % に減少しました。

set_values のコールスタックはそれをよく表しています:_first_block_timestamp 関数はあまりにも高速で、ディスプレイから完全に消えてしまったので見ることさえできません。

ですから、Carbonara への値の挿入プロセス全体を、数分間で25 % 高速化することができました。 最初の素朴なパスとしては悪くないですよね。

より詳しく知りたい場合は、Scaling Python のコードの最適化に関する章を全部書きました。 それをチェックしてください!

コメントを残す

メールアドレスが公開されることはありません。