问题:为什么Python代码在函数中运行得更快?
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
为什么是这样?
回答 0
您可能会问为什么存储局部变量比全局变量更快。这是CPython实现的细节。
请记住,CPython被编译为字节码,解释器将运行该字节码。编译函数时,局部变量存储在固定大小的数组(不是 a dict
)中,并且变量名称分配给索引。这是可能的,因为您不能将局部变量动态添加到函数中。然后检索一个本地变量实际上是对列表的指针查找,而对refcount的引用PyObject
则是微不足道的。
将此与全局查找(LOAD_GLOBAL
)进行对比,它是dict
涉及哈希等的真实搜索。顺便说一句,这就是为什么需要指定global i
是否要使其成为全局变量的原因:如果曾经在作用域内分配变量,则编译器将发出STORE_FAST
s的访问权限,除非您告知不要这样做。
顺便说一句,全局查找仍然非常优化。属性查找foo.bar
是真的慢的!
这是关于局部变量效率的小插图。
回答 1
在函数内部,字节码为:
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
它是局部的,但在顶层是全局的。
回答 2
除了局部/全局变量存储时间外,操作码预测还使函数运行更快。
正如其他答案所解释的,该函数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 *不会*做出类似的预测。相反,它必须返回到评估循环的顶部,该循环对循环的执行速度有明显的影响。
为了提供有关此优化的更多技术细节,以下是该ceval.c
文件(Python虚拟机的“引擎”)的引文:
一些操作码往往成对出现,因此可以在运行第一个代码时预测第二个代码。例如,
GET_ITER
通常紧随其后FOR_ITER
。并且FOR_ITER
通常后跟STORE_FAST
或UNPACK_SEQUENCE
。验证预测需要对寄存器变量进行一个针对常数的高速测试。如果配对良好,则处理器自己的内部分支谓词成功的可能性很高,从而导致到下一个操作码的开销几乎为零。成功的预测可以节省通过评估循环的旅程,该评估循环包括其两个不可预测的分支,
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页面有大约CPython中的虚拟机是如何工作的更多信息。