为什么Python代码在函数中运行得更快?

为什么Python代码在函数中运行得更快?,python,performance,profiling,benchmarking,cpython,Python,Performance,Profiling,Benchmarking,Cpython,Python中的这段代码在中运行(注意:计时是通过Linux中BASH中的time函数完成的) 但是,如果for循环未放置在函数中 real 0m1.841s user 0m1.828s sys 0m0.012s 然后它会运行更长的时间: for i in xrange(10**8): pass 这是为什么?在函数中,字节码是: real 0m4.543s user 0m4.524s sys 0m0.012s 2 0

Python中的这段代码在中运行(注意:计时是通过Linux中BASH中的time函数完成的)

但是,如果for循环未放置在函数中

real    0m1.841s
user    0m1.828s
sys     0m0.012s
然后它会运行更长的时间:

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

这是为什么?

在函数中,字节码是:

real    0m4.543s
user    0m4.524s
sys     0m0.012s
  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        
在顶层,字节码是:

real    0m4.543s
user    0m4.524s
sys     0m0.012s
  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        
不同的是,它比你的速度快。这是因为在一个函数中,
i
是一个局部函数,但在顶层它是一个全局函数

要检查字节码,请使用。我可以直接反汇编函数,但要反汇编顶级代码,我必须使用。

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

请记住,CPython被编译成字节码,解释器运行字节码。编译函数时,局部变量存储在固定大小的数组中(而不是
dict
),变量名被分配给索引。这是可能的,因为您无法向函数动态添加局部变量。然后,检索局部变量实际上就是在列表中查找指针,并在
PyObject
上增加refcount,这很简单

与此相对应的是全局查找(
LOAD\u global
),它是一个真正的
dict
搜索,涉及哈希等。顺便说一句,这就是为什么如果您希望它是全局的,就需要指定
global i
:如果您分配给作用域内的变量,编译器将发出
STORE\u FAST
s来访问它,除非您告诉它不要这样做

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


这里是局部变量效率的小问题。

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

正如其他答案所解释的,该函数在循环中使用
STORE\u FAST
操作码。下面是函数循环的字节码:

  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        
通常,当程序运行时,Python会一个接一个地执行每个操作码,跟踪堆栈,并在执行每个操作码后对堆栈帧执行其他检查。操作码预测意味着在某些情况下Python能够直接跳到下一个操作码,从而避免了一些开销

在这种情况下,每次Python看到
FOR\u ITER
(循环的顶部),它都会“预测”下一个必须执行的操作码是
STORE\u FAST
。Python然后窥视下一个操作码,如果预测正确,它会直接跳到
STORE\u FAST
。这会将两个操作码压缩为一个操作码

另一方面,
STORE\u NAME
opcode在全局级别的循环中使用。Python在看到此操作码时会做出类似的预测。相反,它必须返回到评估循环的顶部,这对循环的执行速度有明显的影响

为了提供有关此优化的更多技术细节,这里引用文件(Python虚拟机的“引擎”):

有些操作码往往成对出现,因此可以 在运行第一个代码时预测第二个代码。例如
GET\u ITER
后面通常跟着
FOR\u ITER
。对于ITER来说,
通常是
然后是
STORE\u FAST
UNPACK\u SEQUENCE

验证预测需要对寄存器进行一次高速测试 与常数相对的变量。如果配对良好,则 处理器自身的内部分支谓词很可能 成功,导致几乎零开销过渡到 下一个操作码。成功的预测可通过eval循环节省行程 包括它的两个不可预测的分支,
有_ARG
测试和 开关箱。结合处理器的内部分支预测, 一个成功的
预测
会使两个操作码运行得如同 它们是一个单独的新操作码,将身体结合在一起

我们可以在操作码的源代码中看到对
STORE\u FAST
进行预测的确切位置:

国际热核试验堆案例:/《国际热核试验堆操作码案例》
v=顶部();
x=(*v->ob_type->tp_iternext)(v);//x是迭代器中的下一个值
如果(x!=NULL){
推(x);//将x放在堆栈顶部
预测(商店快);//预测商店快将接踵而至-成功!
PREDICT(UNPACK_SEQUENCE);//跳过这个和下面的所有内容
继续;
}
//迭代器正常结束时的错误检查和更多代码
PREDICT
函数扩展为
if(*next_instr==op)转到PRED_##op
,即我们只跳到预测操作码的开头。在这种情况下,我们跳到这里:

PREDICTED_WITH_ARG(STORE_FAST);
case STORE_FAST:
v=POP();//从堆栈中弹出x
SETLOCAL(oparg,v);//将其设置为新的局部变量
转到快速下一个操作码;
局部变量现在已经设置好,下一个操作码即将执行。Python将继续执行iterable,直到它到达末尾,每次都会成功地进行预测


有更多关于CPython虚拟机如何工作的信息。

您实际上是如何计时的?只是一种直觉,不确定它是否正确:我猜这是因为范围。在函数的情况下,会创建一个新的作用域(即一种散列,变量名绑定到它们的值)。如果没有函数,变量就在全局范围内,这时可以找到很多东西,从而减慢循环的速度。@Scharron似乎不是这样。定义的200k假人