为什么无参数函数调用执行得更快?

Why do argument-less function calls execute faster?

我设置了一个简单的自定义函数,它接受一些默认参数 (Python 3.5):

def foo(a=10, b=20, c=30, d=40):
    return a * b + c * d

并在指定或不指定参数值的情况下对它进行不同的定时调用:

不指定参数:

%timeit foo()
The slowest run took 7.83 times longer than the fastest. This could mean that an intermediate result is being cached 
1000000 loops, best of 3: 361 ns per loop

指定参数:

%timeit foo(a=10, b=20, c=30, d=40)
The slowest run took 12.83 times longer than the fastest. This could mean that an intermediate result is being cached 
1000000 loops, best of 3: 446 ns per loop

如您所见,指定参数的调用和未指定参数的调用所需的时间略有增加。在简单的一次性调用中,这可能可以忽略不计,但如果对函数进行大量调用,开销会增加并变得更加明显:

没有参数

%timeit for i in range(10000): foo()
100 loops, best of 3: 3.83 ms per loop

有参数:

%timeit for i in range(10000): foo(a=10, b=20, c=30, d=40)
100 loops, best of 3: 4.68 ms per loop

在 Python 2.7 中存在相同的行为,其中这些调用之间的时间差实际上有点大 foo() -> 291nsfoo(a=10, b=20, c=30, d=40) -> 410ns


为什么会这样?我通常应该尝试避免在调用期间指定参数值吗?

Why does this happen? Should I avoid specifying argument values during calls?

一般不会您能够看到这一点的真正原因是因为您使用的函数 根本不是计算密集型 。因此,在提供参数的情况下发出额外的字节码命令所需的时间可以通过计时检测。

例如,如果您有更密集的函数形式:

def foo_intensive(a=10, b=20, c=30, d=40): 
    [i * j for i in range(a * b) for j in range(c * d)]

在所需时间上几乎没有任何区别:

%timeit foo_intensive()
10 loops, best of 3: 32.7 ms per loop

%timeit foo_intensive(a=10, b=20, c=30, d=40)
10 loops, best of 3: 32.7 ms per loop

即使扩展到更多调用,执行函数体所需的时间也完全超过了额外字节码指​​令引入的小开销。


查看字节码:

查看为每个调用案例生成的字节代码的一种方法是创建一个环绕 foo 并以不同方式调用它的函数。现在,让我们为使用默认参数的调用创建 fooDefault,为指定关键字参数的函数创建 fooKwargs()

# call foo without arguments, using defaults
def fooDefault():
    foo()

# call foo with keyword arguments
def fooKw():
    foo(a=10, b=20, c=30, d=40)

现在 dis we can see the differences in byte code between these. For the default version, we can see that essentially one command is issued (Ignoring POP_TOP which is present in both cases) for the function call, CALL_FUNCTION:

dis.dis(fooDefaults)
  2           0 LOAD_GLOBAL              0 (foo)
              3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)  
              6 POP_TOP
              7 LOAD_CONST               0 (None)
             10 RETURN_VALUE

另一方面,在使用关键字的情况下,发出了另外 8 条 LOAD_CONST 命令 以加载参数名称 (a, b, c, d)和值 (10, 20, 30, 40) 进入值堆栈(即使在这种情况下加载数字 < 256 可能非常快,因为它们被缓存):

dis.dis(fooKwargs)
  2           0 LOAD_GLOBAL              0 (foo)
              3 LOAD_CONST               1 ('a')    # call starts
              6 LOAD_CONST               2 (10)
              9 LOAD_CONST               3 ('b')
             12 LOAD_CONST               4 (20)
             15 LOAD_CONST               5 ('c')
             18 LOAD_CONST               6 (30)
             21 LOAD_CONST               7 ('d')
             24 LOAD_CONST               8 (40)
             27 CALL_FUNCTION         1024 (0 positional, 4 keyword pair)
             30 POP_TOP                             # call ends
             31 LOAD_CONST               0 (None)
             34 RETURN_VALUE

此外,对于关键字参数不为零的情况,通常需要一些额外的步骤。 (例如 ceval/_PyEval_EvalCodeWithName())。

尽管这些命令非常快,但它们确实总结了。参数越多,总和越大,当实际执行对函数的多次调用时,这些参数会堆积起来,导致执行时间有所不同。


这些的直接结果是我们指定的值越多,必须发出的命令越多,函数运行速度越慢。此外,指定位置参数、解包位置参数和解包关键字参数都有不同的开销:

  1. 位置参数 foo(10, 20, 30, 40): 需要 4 个额外的命令来加载每个值。
  2. 列表解包foo(*[10, 20, 30, 40]):4个LOAD_CONST命令和一个额外的BUILD_LIST命令。
    • 使用 foo(*l) 中的列表会稍微减少执行,因为我们提供了一个包含值的已构建列表。
  3. 字典解包foo(**{'a':10, 'b':20, 'c': 30, 'd': 40}):8个LOAD_CONST命令和一个BUILD_MAP
    • 与列表解包一样 foo(**d) 将减少执行,因为将提供构建的列表。

总的来说,不同case调用的执行时间排序是:

defaults < positionals < keyword arguments < list unpacking < dictionary unpacking

我建议在这些情况下使用 dis.dis 并查看它们的差异。


结论:

正如@goofd 在评论中指出的那样,这确实是一个不应该担心的事情,它确实取决于用例。如果您从计算的角度经常调用 'light' 函数,则指定默认值会略微提高速度。如果您经常提供不同的值,这几乎不会产生任何结果。

因此,它可能可以忽略不计,并且试图从像这样的模糊边缘案例中获得提升确实在推动它。如果你发现自己在这样做,你可能想看看像 PyPy and Cython.

这样的东西