the*_*tar 809 python performance benchmarking profiling cpython
def main():
for i in xrange(10**8):
pass
main()
Run Code Online (Sandbox Code Playgroud)
Python中的这段代码运行(注意:时序是在Linux中的BASH中使用时间函数完成的.)
real 0m1.841s
user 0m1.828s
sys 0m0.012s
Run Code Online (Sandbox Code Playgroud)
但是,如果for循环没有放在函数中,
for i in xrange(10**8):
pass
Run Code Online (Sandbox Code Playgroud)
然后它会运行更长的时间:
real 0m4.543s
user 0m4.524s
sys 0m0.012s
Run Code Online (Sandbox Code Playgroud)
为什么是这样?
eca*_*mur 651
在函数内部,字节码是
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
Run Code Online (Sandbox Code Playgroud)
在顶层,字节码是
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
Run Code Online (Sandbox Code Playgroud)
区别在于STORE_FAST比(.)更快(!)STORE_NAME.这是因为在一个函数中,它i是一个局部但是在顶层它是一个全局的.
要检查字节码,请使用该dis模块.我能够直接反汇编函数,但是要反汇编顶层代码我必须使用compile内置函数.
Kat*_*iel 508
您可能会问为什么存储局部变量比全局变量更快.这是一个CPython实现细节.
请记住,CPython被编译为字节码,解释器运行.编译函数时,局部变量存储在固定大小的数组(而不是 a dict)中,并且变量名称将分配给索引.这是可能的,因为您无法动态地将局部变量添加到函数中.然后检索一个局部变量实际上是一个指向列表的指针查找,并且refcount的增加PyObject就是微不足道的.
将此与全局lookup(LOAD_GLOBAL)进行对比,这是一个dict涉及哈希等的真正搜索.顺便说一句,这就是为什么你需要指定global i你是否希望它是全局的:如果你曾经分配给一个范围内的变量,编译器将发出STORE_FASTs来进行访问,除非你告诉它不要.
顺便说一句,全局查找仍然相当优化.属性查找foo.bar是真的慢的!
这是关于局部变量效率的小例子.
Ale*_*ley 38
除了局部/全局变量存储时间之外,操作码预测使功能更快.
正如其他答案所解释的那样,该函数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
Run Code Online (Sandbox Code Playgroud)
通常,当程序运行时,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.验证预测会花费一个寄存器变量对常量的高速测试.如果配对良好,则处理器自己的内部分支预测具有很高的成功可能性,导致到下一个操作码的开销几乎为零.成功的预测可以节省通过eval-loop的行程,包括其两个不可预测的分支,
HAS_ARG测试和switch-case.结合处理器的内部分支预测,成功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
Run Code Online (Sandbox Code Playgroud)
该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;
Run Code Online (Sandbox Code Playgroud)
现在设置了局部变量,并且下一个操作码已启动执行.Python继续通过迭代直到它到达终点,每次都成功进行预测.
在Python的wiki页面有大约CPython中的虚拟机是如何工作的更多信息.