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
どうしてこれなの?
あなたはなぜがグローバル変数よりローカル変数を格納するほうが速いのか尋ねるかもしれません。これはCPythonの実装の詳細です。
CPythonは、インタプリタが実行するバイトコードにコンパイルされていることを忘れないでください。関数がコンパイルされると、ローカル変数は固定サイズの配列(dict
)に格納され、変数名がインデックスに割り当てられます。これは可能です。ローカル変数を関数に動的に追加することはできないからです。ローカル変数を検索することは文字通りリストへのポインタルックアップと些細なPyObject
のrefcountの増加です。
これをグローバルルックアップ(LOAD_GLOBAL
)と比較してください。これは、ハッシュなどを含む真のdict
検索です。ちなみに、これがグローバルにしたい場合はglobal i
を指定する必要がある理由です。スコープ内の変数に代入したことがある場合は、そうでないことを指示しない限り、コンパイラはアクセスのためにSTORE_FAST
sを発行します。
ところで、グローバルルックアップはまだかなり最適化されています。属性検索foo.bar
は本当に遅いものです!
以下は、局所変数の効率に関する小さな 図 です。
関数内では、バイトコードは
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
組み込み関数 を使わなければなりませんでした。
ローカル/グローバル変数の格納時間は別として、オペコード予測は関数をより速くします。
他の答えが説明するように、関数はループ内で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の仮想マシンがどのように機能するかについての詳細があります。