ローカル/グローバル変数の保存時間は別として、オペコード予測により関数が高速になります。
他の回答が説明しているように、関数は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
ます。
予測の検証には、定数に対するレジスタ変数の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の仮想マシンがどのように機能するかについての詳細があります。