2012-06-28 8 views
699
def main(): 
    for i in xrange(10**8): 
     pass 
main() 

Pythonでコードのこの部分は、で実行され(注:タイミングはLinuxでBASHにおける時間関数を用いて行われる。)Pythonコードが関数内で高速に動作するのはなぜですか?

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 

なぜこれがあります?関数の内部

+10

実際にタイミングをどのようにしましたか? –

+0

Python 3.2.3 REPLの動作が確認されました。面白い。 – Deestan

+39

ちょうど直感、それが本当であるかどうかわからない:私はそれがスコープのためだと思うだろう。関数の場合、新しいスコープが作成されます(つまり、値にバインドされた変数名を持つハッシュの種類)。関数がなければ、変数はグローバルスコープにあります。多くのものが見つかると、ループが遅くなります。 – Scharron

答えて

414

なぜかなぜグローバル変数よりもローカル変数を保存する方が速いですか。これはCPython実装の詳細です。

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

これは、グローバル検索(LOAD_GLOBAL)と対照的です。これは、ハッシュを含む真の検索結果であるdictです。ちなみに、これをグローバルにしたいのであれば、global iを指定する必要があります。スコープ内の変数に割り当てることができない場合は、コンパイラはSTORE_FASTを発行します。

ところで、グローバルルックアップはまだかなり最適化されています。属性の参照foo.barは、となります。実際にはです。

ここでは、ローカル変数の効率が小さいillustrationです。

+5

これはPyPyにも当てはまります。現在のバージョン(この執筆時点では1.8)。OPのテストコードは、関数の内部と比較してグローバルスコープで約4倍遅く実行されます。 – GDorn

+0

これはjavascriptのスコープ解決に関するビデオで言及したのと同じです。私は、グローバル変数の検索がローカル変数のプロパティの検索より速いとは考えていませんでした。今日何か新しいことを学びました! – mowwwalker

+3

@Walkerneoあなたはそれを逆に言っていない限り、彼らはそうではありません。 katrielalexとecatmurが言っていることによると、グローバル変数の参照は、格納の方法のためにローカル変数の参照よりも遅いです。 –

621

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

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   

で、バイトコードは差がSTORE_FASTSTORE_NAMEよりも(!)高速であるということである

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   

です。これは、関数内ではiがローカルですが、トップレベルではグローバルであるためです。

バイトコードを調べるには、dis moduleを使用してください。私は直接関数を逆アセンブルすることができましたが、私はcompile builtinを使用しなければならなかったトップレベルコードを逆アセンブルしました。

+155

実験で確認しました。 'main'関数に' global'を挿入すると、実行時間は同等になります。 – Deestan

+44

これは疑問に答えることなく質問に答えます:)ローカル関数変数の場合、CPythonは実際には辞書を要求するまで(例えば 'locals()'を使って、これらをタプル(Cコードから変更可能)に格納します。 'inspect.getframe()'など)。一定の整数で配列要素を調べることは、dictを検索するよりもはるかに高速です。 – dmw

+3

C/C++と同じですが、グローバル変数を使用するとかなりの減速が発生します – codejammer

28

ローカル/グローバル変数のストア時間以外に、オペコードの予測は、機能を高速化します。

他の答えが説明するように、関数はループ内で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となります。

予測の検証では、レジスタの高速高速テストである が定数に対して変わります。ペアリングが良好だった場合、 プロセッサ自身の内部分岐予測は、 の成功確率が高く、次のオペコード にほぼゼロオーバヘッドで遷移します。予測が成功すると、eval-loop に2つの予期しない分岐、HAS_ARGテストと スイッチケースが含まれています。プロセッサの内部分岐予測と組み合わせると、 成功したPREDICTは、2つのオペコードを のように実行する効果があります。これらは、ボディを結合した単一の新しいオペコードです。私たちは予測しオペコードの先頭にジャンプすなわちPREDICT機能がif (*next_instr == op) goto PRED_##opに展開

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          

:私たちはSTORE_FASTの予測が行われた正確FOR_ITERオペコードのソースコードに見ることができます

。この場合、ここでジャンプします:

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 pageには、CPythonの仮想マシンがどのように機能するかについての詳細があります。

関連する問題