关于python:函数调用执行速度比非函数调用快

Function call execution speed is faster than non-function call

函数调用总是会产生一些开销。但是为什么下面的代码显示非函数调用较慢。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time

def s():
    for i in range(1000000000):
        1 + 1

t = time.time()
s()
print("Function call:" + str(time.time() - t))

t = time.time()
for i in range(1000000000):
    1 + 1
print("Non function call:" + str(time.time() - t))

输出:

1
2
Function call: 38.39736223220825
Non function call: 60.33238506317139


您可能会认为,由于循环只执行1 + 1,所以不应该有太大的差异。但是,这里有一个通常被遗忘的"隐藏"赋值:循环变量i在你的for循环中。这就是减速的原因。

在函数中,这是通过STORE_FAST完成的。在顶层,它是用STORE_NAME完成的。第一个比另一个更快,在一个运行1000000000次的循环中,这个差异显示得非常清楚。

记住,函数调用只发生一次。所以它的开销在这个特定的场景中并没有真正的作用。

除此之外,所有其他步骤都发生过一次,而且几乎是相同的。创建一个范围,获取其迭代器,并为每个迭代加载常量2

正如@moses在评论中指出的那样,您总是可以使用dis模块来检查为每一个模块生成的cpython字节码。对于s功能,您有:

1
2
3
4
5
6
7
8
dis.dis(s)
#       snipped for brevity
        >>   10 FOR_ITER                 8 (to 20)
             12 STORE_FAST               0 (i)

  3          14 LOAD_CONST               3 (2)
             16 POP_TOP
             18 JUMP_ABSOLUTE           10

而对于循环的顶级版本:

1
2
3
4
5
6
7
dis('for i in range(1000000000): 1+1')
#       snipped for brevity
        >>   10 FOR_ITER                 8 (to 20)
             12 STORE_NAME               1 (i)
             14 LOAD_CONST               3 (2)
             16 POP_TOP
             18 JUMP_ABSOLUTE           10

它们之间的主要区别在于存储迭代值i。在功能上,它只是更有效。

要解决@reblochon masque(现已删除)的问题,在ipython细胞中,当与timeit同步时,这两个答案似乎没有差异。

timeit通过创建一个小函数(名为inner)来乘以事物,该函数存储您传递的语句,并在给定数量的执行中执行它们。如果您创建一个Timer对象并查看它的src属性(这没有文档记录,所以不要期望它总是存在于该属性中),就可以看到这一点:

1
2
3
4
from timeit import Timer

t = Timer('for i in range(10000): 1 + 1')
print(t.src)

它包含基本上是定时的小函数。以前的print呼叫打印:

1
2
3
4
5
6
7
def inner(_it, _timer):
    pass
    _t0 = _timer()
    for _i in _it:
        for i in range(10000): 1 + 1
    _t1 = _timer()
    return _t1 - _t0

因此,实际上,通过使用timeit,您已经改变了执行i查找的方式,因为它位于函数内部,也可以使用STORE_FAST进行查找。容易陷阱!

(如果你不相信我,见《以东记》〔19〕)