除了本地/全局变量存储时间之外,操作码预测使函数更快。
正如其他答案所解释的,该函数在循环中使用 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。这具有将两个操作码压缩成一个操作码的效果。
另一方面,STORE_NAME 操作码用于全局级别的循环中。当 Python 看到此操作码时,*not* 会做出类似的预测。相反,它必须回到评估循环的顶部,这对循环的执行速度有明显的影响。
为了提供有关此优化的更多技术细节,这里引用ceval.c 文件(Python 虚拟机的“引擎”):
一些操作码往往成对出现,因此可以
在运行第一个代码时预测第二个代码。例如,
GET_ITER 后面经常跟 FOR_ITER。而 FOR_ITER 经常是
后跟STORE_FAST 或UNPACK_SEQUENCE。
验证预测需要对寄存器进行一次高速测试
变量对常数。如果配对很好,那么
处理器自己的内部分支预测很有可能
成功,导致几乎零开销过渡到
下一个操作码。成功的预测节省了通过 eval 循环的行程
包括它的两个不可预测的分支,HAS_ARG 测试和
开关盒。结合处理器的内部分支预测,
成功的PREDICT 具有使两个操作码像运行一样的效果
它们是结合了主体的单个新操作码。
我们可以在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 page 有更多关于 CPython 的虚拟机如何工作的信息。