web-dev-qa-db-ja.com

Python cProfileから意味のある結果を取得する

ファイルにPythonスクリプトがあり、実行に30秒強かかります。今回は大幅に削減したいので、プロファイルを作成しようとしています。

私はcProfileを使用してスクリプトのプロファイルを作成しようとしていますが、基本的には、メインスクリプトの実行に長い時間がかかりましたが、期待していた種類の内訳は表示されません。ターミナルで、次のように入力します。

cat my_script_input.txt | python -m cProfile -s time my_script.py

私が得る結果は次のとおりです。

<my_script_output>
             683121 function calls (682169 primitive calls) in 32.133 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1   31.980   31.980   32.133   32.133 my_script.py:18(<module>)
   121089    0.050    0.000    0.050    0.000 {method 'split' of 'str' objects}
   121090    0.038    0.000    0.049    0.000 fileinput.py:243(next)
        2    0.027    0.014    0.036    0.018 {method 'sort' of 'list' objects}
   121089    0.009    0.000    0.009    0.000 {method 'strip' of 'str' objects}
   201534    0.009    0.000    0.009    0.000 {method 'append' of 'list' objects}
   100858    0.009    0.000    0.009    0.000 my_script.py:51(<lambda>)
      952    0.008    0.000    0.008    0.000 {method 'readlines' of 'file' objects}
 1904/952    0.003    0.000    0.011    0.000 fileinput.py:292(readline)
    14412    0.001    0.000    0.001    0.000 {method 'add' of 'set' objects}
      182    0.000    0.000    0.000    0.000 {method 'join' of 'str' objects}
        1    0.000    0.000    0.000    0.000 fileinput.py:80(<module>)
        1    0.000    0.000    0.000    0.000 fileinput.py:197(__init__)
        1    0.000    0.000    0.000    0.000 fileinput.py:266(nextfile)
        1    0.000    0.000    0.000    0.000 {isinstance}
        1    0.000    0.000    0.000    0.000 fileinput.py:91(input)
        1    0.000    0.000    0.000    0.000 fileinput.py:184(FileInput)
        1    0.000    0.000    0.000    0.000 fileinput.py:240(__iter__)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

これは有用なことを何も言っていないようです。大半の時間は、単に次のようにリストされています。

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1   31.980   31.980   32.133   32.133 my_script.py:18(<module>)

my_script.py、18行目は終了"""のファイルのヘッダーブロックコメント。したがって、18行目に集中して作業が集中するわけではありません。スクリプト全体は、ほとんどが文字列の分割、並べ替え、設定作業を伴う行ベースの処理で構成されています。そのため、これらのアクティビティの1つ以上に多くの時間を費やすことを期待していました。現状では、cProfileの結果でグループ化されたすべての時間をコメント行で発生していると見ることは意味をなさないか、少なくとも実際に常に消費しているものに光を当てません。

編集:同じ動作を示すために、上記の場合と同様の最小限の作業例を作成しました。

mwe.py

import fileinput

for line in fileinput.input():
    for i in range(10):
        y = int(line.strip()) + int(line.strip())

そしてそれを次のように呼び出します:

Perl -e 'for(1..1000000){print "$_\n"}' | python -m cProfile -s time mwe.py

結果を取得するには:

         22002536 function calls (22001694 primitive calls) in 9.433 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    8.004    8.004    9.433    9.433 mwe.py:1(<module>)
 20000000    1.021    0.000    1.021    0.000 {method 'strip' of 'str' objects}
  1000001    0.270    0.000    0.301    0.000 fileinput.py:243(next)
  1000000    0.107    0.000    0.107    0.000 {range}
      842    0.024    0.000    0.024    0.000 {method 'readlines' of 'file' objects}
 1684/842    0.007    0.000    0.032    0.000 fileinput.py:292(readline)
        1    0.000    0.000    0.000    0.000 fileinput.py:80(<module>)
        1    0.000    0.000    0.000    0.000 fileinput.py:91(input)
        1    0.000    0.000    0.000    0.000 fileinput.py:197(__init__)
        1    0.000    0.000    0.000    0.000 fileinput.py:184(FileInput)
        1    0.000    0.000    0.000    0.000 fileinput.py:266(nextfile)
        1    0.000    0.000    0.000    0.000 {isinstance}
        1    0.000    0.000    0.000    0.000 fileinput.py:240(__iter__)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

どういうわけかcProfileを間違って使用していますか?

32
Bryce Thomas

コメントで述べたように、cProfileを外部で動作させることができない場合、代わりに内部で使用することができます。そんなに難しくありません。

たとえば、Python 2.7)で-m cProfileを使用して実行すると、実際に同じ結果が得られます。しかし、サンプルプログラムを手動でインストルメントすると:

import fileinput
import cProfile

pr = cProfile.Profile()
pr.enable()
for line in fileinput.input():
    for i in range(10):
        y = int(line.strip()) + int(line.strip())
pr.disable()
pr.print_stats(sort='time')

…ここに私が得るものがある:

         22002533 function calls (22001691 primitive calls) in 3.352 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
 20000000    2.326    0.000    2.326    0.000 {method 'strip' of 'str' objects}
  1000001    0.646    0.000    0.700    0.000 fileinput.py:243(next)
  1000000    0.325    0.000    0.325    0.000 {range}
      842    0.042    0.000    0.042    0.000 {method 'readlines' of 'file' objects}
 1684/842    0.013    0.000    0.055    0.000 fileinput.py:292(readline)
        1    0.000    0.000    0.000    0.000 fileinput.py:197(__init__)
        1    0.000    0.000    0.000    0.000 fileinput.py:91(input)
        1    0.000    0.000    0.000    0.000 {isinstance}
        1    0.000    0.000    0.000    0.000 fileinput.py:266(nextfile)
        1    0.000    0.000    0.000    0.000 fileinput.py:240(__iter__)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

それははるかに便利です:おそらくあなたがすでに期待していたこと、つまりあなたの時間の半分以上がstr.strip()の呼び出しに費やされていることを示します。


また、プロファイリングするコードを含むファイル(mwe.py)を編集できない場合は、いつでもこれを実行できることに注意してください。

import cProfile
pr = cProfile.Profile()
pr.enable()
import mwe
pr.disable()
pr.print_stats(sort='time')

それでさえ常に機能するとは限りません。たとえば、プログラムがexit()を呼び出す場合、try:/finally:ラッパーまたはatexitを使用する必要があります。そして、それはos._exit()またはsegfaultsを呼び出し、おそらくあなたは完全にホースでくまれるでしょう。しかし、それはあまり一般的ではありません。


ただし、後で発見したことがあります。すべてのコードをグローバルスコープから移動すると、少なくともこの場合は-m cProfileが機能しているように見えます。例えば:

import fileinput

def f():
    for line in fileinput.input():
        for i in range(10):
            y = int(line.strip()) + int(line.strip())
f()

現在、-m cProfileからの出力には、特に次のものが含まれています。

2000000 4.819 0.000 4.819 0.000:0(ストリップ)100001 0.288 0.000 0.295 0.000 fileinput.py:243(next)

なぜこれが2倍遅くなったのか、私にはわかりません。最後に実行してから数分経ち、その間に多くのWebブラウジングを行ってきました。しかし、それは重要ではありません。重要なことは、ほとんどの場合、合理的な場所に請求されることです。

しかし、これを変更して外側のループをグローバルレベルに移動し、その本体のみを関数にすると、ほとんどの時間は再び消えます。


別の代替手段。最後の手段として以外は提案しません…

CProfileの代わりにプロファイルを使用すると、内部と外部の両方で機能し、適切な呼び出しに時間を費やすことに気付きます。ただし、これらの呼び出しも約5倍遅くなります。さらに、10秒の一定のオーバーヘッドが追加されているようです(内部で使用される場合はimport profileに、外部で使用される場合は1行目で何が発生します)。したがって、splitが私の時間の70%を使用していることを確認するには、4秒待機して2.326/3.352を実行する代わりに、27秒待機して10.93 /(26.34-10.01)を実行する必要があります。あまり面白くない…


最後に、CPython 3.4開発ビルドでも同じ結果が得られます。内部で使用すると正しい結果が得られ、外部で使用するとすべてがコードの最初の行に課金されます。しかし、PyPy 2.2/2.7.3とPyPy3 2.1b1/3.2.3はどちらも-m cProfileで正しい結果を提供するようです。これは、PyPyのcProfileprofileの上に偽装されていることを意味している可能性があります。これは、純粋なPythonコードが十分に高速だからです。


とにかく、誰かが-m cProfileが動作しない理由を理解/説明できるなら、それは素晴らしいことでしょう…しかし、そうでなければ、これは通常完全に良い回避策です。

50
abarnert