PyPy における Python のパフォーマンスチューニング

これは PyPy Advent Calendar の記事です。PyPyのコアディベロッパーである "Maciej Fijalkowski" 氏のブログ "Analysing python's performance under PyPy" の抄訳+αです。

Python の一般的なパフォーマン解析のモデルは、"プロファイラを実行して、ボトルネックを探し出し、それを最適化するか C で書き直す" ことです。しかし PyPy ではこのアプローチだけでは不十分です。なぜなら、

  • 多くの大規模アプリケーションで、プロファイラはフラットです: PyPy のトランスレーションツールチェーン、Twisted、モダンな Web サーバ等が良い例です
  • ボトルネックを発見したとしても、それが特定の関数内でのみ遅いのか、複数の関数が関係しているのか明確になるわけではありません。どうすれば遅くて、どうすれば速くなるかは CPython においても明確な答えはありません。JIT が適用されるとさらに複雑です。 JIT が特定のコードをどのようにコンパイルしたかを確認することが重要になります。
  • パフォーマンスにおいては、特に GC 関連の問題は多くの関数に影響がありますが、プロファイルでは確認できません。

PyPy には、問題を解決するためのいくつかのツールが提供されています。プログラムのパフォーマン解析に関するいくつかの方法を示します。これはガイドラインであり、銀の弾丸ではありません。アプリケーションが複雑な場合は、多くの鉛の弾丸が必要でしょう。

>>>> テストを作成する

これは品質に関するものではありません。多くの自動化されたテストを受けることで、その機能を失うことなく、よりパフォーマンスの高いコードにリファクタできるようにします。

>>>> ベンチマークを書く

これが重要な出発点となります。ひとつのスクリプトで、できれば引数を指定して、変更の影響を測定できるようにする必要があります。

1 回だけしか実行されないスクリプトでない場合は、同じテストを繰り返し実行することで JIT のウォームアップ時間がパフォーマンスにどのような影響があるかを測定することができます。それは連続して実行されるとどのように変化するかを視覚化する助けにもなります。

"Maciej Fijalkowski" さんのベンチマークは、ステップごとに 0.2 秒から 5 秒実行しているそうです。これにより、誤差を最小限にすることができます。JIT のウォーミングアップ時間は、コードベースによって違ってきます。それは一瞬かもしれませんし、1 分かもしれません。

>>>> cProfile の結果を考慮する

Python のプロファイラ (cProfile) をカスタマイズした lsprofcalltree.py を利用し、kcachegrid へ取り込み可能なフォーマットで出力します。これは役に立つ情報を提供するかもしれませんし、しないかもしれません。プロファイルから突出している関数がある場合に、その効率やアルゴリズムを確認します。

>>>> GC や JIT 等の比率を確認する

PyPy のコードベースには、この確認に便利なツールが提供されています。プログラムを pypy virtualenv で実行している場合は:

$ PYPYLOG=log ./test.py

を実行します。また、pypy のリポジトリのチェックアウトから:

$ pypy/tool/logparser.py print-summary log -

を実行します。また、グラフで確認したい場合は:

$ pypy/tool/logparser.py draw-time log out.png

を実行します。これは、だいたい何にどれぐらいの時間が費やされているかを確認することができます。GC、JIT トレース (ウォームアップ時間)、その他 JIT 化されたコードの実行時間が含まれています。

logparser の出力例については、PyPy Advent calendar: 7 日目の id:rokujyouhitoma (総帥) 先生のブログが参考になります。

>>>> jitviewer を利用する

jitviewer により、コードに何が起きたかを大まかに確認することができます。

インストール方法 (Ubuntu)

以下がインストールされていることが前提です。

  • virtualenvwrapper or virtualenv
  • pypy-1.7

    ※ PyPy は /opt/ 配下にインストールし、 /usr/bin にシンボリックリンク作ってます。

    cd /opt/
    wget https://bitbucket.org/pypy/pypy/downloads/pypy-1.7-linux.tar.bz2
    tar -jxf pypy-1.7-linux.tar.bz2
    sudo ln -s /opt/pypy-1.7/bin/pypy /usr/bin/
    

jitviewer 用の virtualenv を作成し、インストールします。

mkvirtualenv pypy-viewer -p /usr/bin/pypy
pip install JitViewer
# 開発版を使われたい方: pip install -e hg+https://bitbucket.org/pypy/jitviewer/#egg=jitviewer

最後に PYTHONPATH に /opt/pypy-1.7/lib_pypy, lib-python, py を追加します。

こんな感じで JIT ログ + アセンブラを確認することができます (これを見て盛り上がる pypyja のチャット)。

>>>> JIT にやさしいコード

本家にある Wiki翻訳しました。以下抜粋します。

  • 属性名は一定にする: setattr(x, 'a' + some_variable, y) よりも setattr(x, 'a', y) の方が高速
  • 新クラス形式を使う / クラスを継承させない
  • 関数の引数の指定で *args**kwargs を使わない: 内部コードが増える
  • JIT を無効化するコードは書かない: logging モジュール, トレース, フレームイントロスペクション (sys._getframe(), sys.exc_info()

現状は遅いけど改善に取り組んでるコード。

  • ジェネレータよりリスト内包表記が高速 -> ジェネレータの高速化
  • str.join(list) より cStringIO が高速 -> str の高速化

チューニングはひたすらベンチマークと格闘のようですね。次は @shomah4a 先生です。

コメント

このブログの人気の投稿

Python から Win32 API 経由で印刷する

Disqus のスケール - Django 編

#PySpa アドベント (23 日目)