web-dev-qa-db-ja.com

Pythonコードが関数内で高速に実行されるのはなぜですか?

def main():
    for i in xrange(10**8):
        pass
main()

Pythonのこのコードは、で実行されます(注:タイミングはLinuxのBASHのtime関数で行われます)。

real    0m1.841s
user    0m1.828s
sys     0m0.012s

ただし、forループが関数内に配置されていない場合は、

for i in xrange(10**8):
    pass

それからそれははるかに長い時間実行されます。

real    0m4.543s
user    0m4.524s
sys     0m0.012s

どうしてこれなの?

788
thedoctar

あなたはなぜがグローバル変数よりローカル変数を格納するほうが速いのか尋ねるかもしれません。これはCPythonの実装の詳細です。

CPythonは、インタプリタが実行するバイトコードにコンパイルされていることを忘れないでください。関数がコンパイルされると、ローカル変数は固定サイズの配列(dict)に格納され、変数名がインデックスに割り当てられます。これは可能です。ローカル変数を関数に動的に追加することはできないからです。ローカル変数を検索することは文字通りリストへのポインタルックアップと些細なPyObjectのrefcountの増加です。

これをグローバルルックアップ(LOAD_GLOBAL)と比較してください。これは、ハッシュなどを含む真のdict検索です。ちなみに、これがグローバルにしたい場合はglobal iを指定する必要がある理由です。スコープ内の変数に代入したことがある場合は、そうでないことを指示しない限り、コンパイラはアクセスのためにSTORE_FASTsを発行します。

ところで、グローバルルックアップはまだかなり最適化されています。属性検索foo.bar本当に遅いものです!

以下は、局所変数の効率に関する小さな です。

493
Katriel

関数内では、バイトコードは

  2           0 SETUP_LOOP              20 (to 23)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_FAST               0 (i)

  3          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               0 (None)
             26 RETURN_VALUE        

トップレベルでは、バイトコードは

  1           0 SETUP_LOOP              20 (to 23)
              3 LOAD_NAME                0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_NAME               1 (i)

  2          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               2 (None)
             26 RETURN_VALUE        

違いは、 STORE_FASTSTORE_NAME よりも速い(!)ということです。これは、関数内ではiがローカルだがトップレベルではグローバルだからです。

バイトコードを調べるには、 disモジュール を使います。関数を直接逆アセンブルすることはできましたが、トップレベルのコードを逆アセンブルするためには compile組み込み関数 を使わなければなりませんでした。

648
ecatmur

ローカル/グローバル変数の格納時間は別として、オペコード予測は関数をより速くします。

他の答えが説明するように、関数はループ内でSTORE_FASTオペコードを使用します。これが関数のループのバイトコードです。

    >>   13 FOR_ITER                 6 (to 22)   # get next value from iterator
         16 STORE_FAST               0 (x)       # set local variable
         19 JUMP_ABSOLUTE           13           # back to FOR_ITER

通常、プログラムが実行されると、Pythonは各オペコードを次々に実行し、スタックを追跡し、各オペコードが実行された後にスタックフレームで他のチェックを実行します。オペコード予測とは、場合によってはPythonが次のオペコードに直接ジャンプできるため、このオーバーヘッドの一部を回避できることを意味します。

この場合、PythonがFOR_ITER(ループの先頭)を見つけるたびに、STORE_FASTが次に実行する必要があるオペコードであることが「予測」されます。次にPythonは次のオペコードを覗き見し、予測が正しかった場合はSTORE_FASTに直接ジャンプします。これには、2つのオペコードを1つのオペコードに絞り込む効果があります。

一方、STORE_NAMEオペコードはループ内でグローバルレベルで使用されます。このオペコードを見たとき、Pythonは*しない*同様の予測をします。代わりに、それは評価ループの先頭に戻る必要があります。これは、ループが実行される速度に明らかに影響します。

この最適化についてもう少し技術的な詳細を説明するために、 ceval.c ファイル(Pythonの仮想マシンの「エンジン」)からの引用を次に示します。

一部のオペコードはペアになる傾向があるため、最初のコードが実行されたときに2番目のコードを予測できるようになります。たとえば、GET_ITERの後にFOR_ITERが続くことがよくあります。そしてFOR_ITERの後にはSTORE_FASTまたはUNPACK_SEQUENCEがしばしば続きます。

予測を検証するには、定数に対するレジスタ変数の単一の高速テストが必要です。ペアリングが良好であれば、プロセッサ自身の内部分岐予測が成功する可能性が高く、次のオペコードへのオーバーヘッドはほぼゼロになります。予測が成功すると、2つの予測不可能な分岐HAS_ARGテストとswitch-caseを含むevalループを通過する手間が省けます。プロセッサの内部分岐予測と組み合わせると、成功したPREDICTは、2つのオペコードをあたかもそれらが本体を組み合わせた単一の新しいオペコードであるかのように実行するという効果があります。

FOR_ITER オペコードのソースコードで、STORE_FASTの予測が正確に行われている場所を確認できます。

case FOR_ITER:                         // the FOR_ITER opcode case
    v = TOP();
    x = (*v->ob_type->tp_iternext)(v); // x is the next value from iterator
    if (x != NULL) {                     
        Push(x);                       // put x on top of the stack
        PREDICT(STORE_FAST);           // predict STORE_FAST will follow - success!
        PREDICT(UNPACK_SEQUENCE);      // this and everything below is skipped
        continue;
    }
    // error-checking and more code for when the iterator ends normally                                     

PREDICT関数はif (*next_instr == op) goto PRED_##opに展開されます。つまり、予測オペコードの先頭にジャンプするだけです。この場合は、ここでジャンプします。

PREDICTED_WITH_ARG(STORE_FAST);
case STORE_FAST:
    v = POP();                     // pop x back off the stack
    SETLOCAL(oparg, v);            // set it as the new local variable
    goto fast_next_opcode;

ローカル変数が設定され、次のオペコードが実行されます。 Pythonは反復可能オブジェクトを最後まで処理し続け、毎回予測を成功させます。

Python wikiページ には、CPythonの仮想マシンがどのように機能するかについての詳細があります。

37
Alex Riley