892
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

どうしてこれなの?

4

3 に答える 3

681

関数内のバイトコードは次のとおりです。

  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_FASTよりも速い(!)ということSTORE_NAMEです。これは、関数でiはローカルですが、トップレベルではグローバルであるためです。

バイトコードを調べるには、disモジュールを使用します。関数を直接分解することはできましたが、トップレベルのコードを分解するには、compile組み込みのを使用する必要がありました。

于 2012-06-28T09:29:12.917 に答える
573

グローバル変数よりもローカル変数を格納する方が速いのはなぜかと疑問に思うかもしれません。これはCPython実装の詳細です。

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

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

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

これは、局所変数の効率に関する小さな図です。

于 2012-06-28T10:15:08.067 に答える
46

ローカル/グローバル変数の保存時間は別として、オペコード予測により関数が高速になります。

他の回答が説明しているように、関数は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_FASTUNPACK_SEQUENCEます。

予測の検証には、定数に対するレジスタ変数の1回の高速テストが必要です。ペアリングが良好である場合、プロセッサ自体の内部分岐予測は成功する可能性が高く、次のオペコードへのオーバーヘッドがほぼゼロになります。予測が成功すると、2つの予測不可能なブランチ、HAS_ARGテストとスイッチケースを含む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は、最後に到達するまでiterableを続行し、毎回予測を成功させます。

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

于 2015-04-05T18:45:34.743 に答える