performance - 函数Python的代码为什么运行得更快?

  显示原文与译文双语对照的内容

def main():
 for i in xrange(10**8):
 pass
main()

python 中的这段代码运行于


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

为什么是这样?

注意:时间是在Linux中的BASH时间函数中完成的。

时间:

你可能会问为什么存储局部变量比全局变量快。 这是一个CPython的实现细节。

记住,CPython编译为字节码,解释器运行它。 编译函数时,局部变量存储在fixed-size数组( dict ) 中,变量名称被分配给索引。 这是可能的,因为你不能动态地向函数添加局部变量。 然后检索一个局部变量实际上是一个指针查找列表,增加了refcount PyObject 这是微不足道的。

对比一个全局查找( LOAD_GLOBAL ),它是一个真正的dict 搜索,包含哈希等。 顺便说一下,这就是为什么你需要指定 global i,如果你希望它是全局的: 如果你曾经在作用域中分配过变量,编译器将为它的访问而发出 STORE_FAST,除非你不告诉它。

顺便说一下,全局查找仍然相当优化。 属性查找 foo.bar 是非常缓慢的 !

在函数中,字节码是


 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 模块 。 我可以直接反汇编该函数,但要反汇编顶级代码,我必须使用 compile 内置 。xml 。

除了局部/全局变量存储时间之外,的操作码预测使函数更快。

就像其他答案解释的那样,函数在循环中使用 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 看到 FOR_ITER ( 循环的顶部) 时,它都会预测 STORE_FAST 是下一个。 然后它会扫视下一个操作码,如果预测正确,它会直接跳转到 STORE_FAST 。 这会将两个操作码压缩成一个操作码并使执行更快。

在全局级别,在循环中使用 STORE_NAME 操作码。 python 在看到这里操作码时不会做出类似的预测。 相反,它必须通过evaluation-loop返回每个操作码,这对执行循环的速度有明显的影响。

为了提供关于这里优化的更多技术细节,下面是 ceval.c 文件("引擎"python 机器的虚拟)的一个引用:

某些操作码趋向成对出现,因此在第一次运行时可以预测第二个代码。 例如 GET_ITER 通常跟在 FOR_ITER 后面。 FOR_ITER 通常是 STORE_FAST 或者 UNPACK_SEQUENCE

验证预测成本一个寄存器变量对常数的单一高速测试。 如果配对良好,那么自己的处理器内部分支预测有很高的成功率,这会导致几乎zero-overhead转换到下一个操作码。 一个成功的预测保存了通过eval-loop的旅程,包括它的两个不可预测的分支,HAS_ARG 测试和 switch-case 。 处理器的内部分支预测相结合,成功 PREDICT 都使这两个操作码运行就像一个新操作码的身体结合。

在源代码中,我们可以看到 FOR_ITER opcode操作码的源代码,它正是对 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 继续通过iterable直到到达末尾,每次都成功地预测。

...