为什么Python代码在函数中运行得更快?
def main(): for i in xrange(10**8): pass main()
Python中的这段代码运行在(注意:在Linux的BASH中,时间函数是用时间函数完成的。)
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
为什么是这样?
你可能会问, 为什么存储局部variables比全局variables更快。 这是一个CPython实现细节。
请记住,CPython被编译为解释器运行的字节码。 编译函数时,局部variables存储在一个固定大小的数组中( 而不是 dict
),variables名称被分配给索引。 这是可能的,因为你不能dynamic添加局部variables到一个函数。 然后检索一个局部variables是字面上的指针查找列表和PyObject
的refcount增加是微不足道的。
将其与全局查找( LOAD_GLOBAL
)进行对比,这是一个包含哈希等的真正的dict
search。 顺便说一句,这就是为什么你需要指定global i
如果你希望它是全局的:如果你曾经指定一个范围内的variables,编译器将发出STORE_FAST
s的访问,除非你不告诉它。
顺便说一下,全局查找仍然相当优化。 属性查找foo.bar
是真的很慢的!
这里是关于局部variables效率的小例子 。
在函数内部,字节码是
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
是一个本地的,但是在顶层是一个全局的。
要检查字节码,请使用dis
模块 。 我能够直接反汇编function,但反汇编顶级代码,我不得不使用compile
内置 。
除了本地/全局variables存储时间之外, 操作码预测使得函数更快。
正如其他答案所解释的,该函数使用循环中的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
。validation预测花费一个寄存器variables对恒定值的单个高速testing。 如果配对是好的,那么处理器自己的内部分支预测成功的可能性很高,导致到下一个操作码的开销几乎为零。 一个成功的预测可以通过包括其两个不可预知的分支,
HAS_ARG
testing和开关情况在内的评估循环来保存一个行程。 结合处理器的内部分支预测,一个成功的PREDICT
具有使两个操作码运行的效果,就好像它们是一个单一的新的操作码一起组合的。
我们可以在FOR_ITER
操作码的源代码中看到FOR_ITER
预测的确切位置:
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;
现在设置了局部variables,下一个操作码开始执行。 Python继续遍历迭代器,直到它结束,每次都成功进行预测。
Python wiki页面提供了关于CPython虚拟机如何工作的更多信息。