ローカル/グローバル変数の格納時間は別として、オペコード予測は関数をより高速にします。
他の答えが説明するように、関数は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は各オペコードを1つずつ実行し、スタックを追跡し、各オペコードの実行後にスタックフレームで他のチェックを実行します。オペコード予測は、特定のケースで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つのオペコードが本体を組み合わせた1つの新しいオペコードであるかのように実行されます。
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のの仮想マシンがどのように機能するかについての詳細な情報を持っています。