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

时间:2016-01-04 17:43:51

标签: python python-2.7 function python-3.x python-internals

我设置了一个简单的自定义函数,它接受一些默认参数(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

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

1 个答案:

答案 0 :(得分:15)

  

为什么会这样?我应该避免在通话期间指定参数值吗?

通常,否您能够看到这个的真正原因是因为您使用的功能根本不是计算密集型 。因此,可以通过计时检测在提供自变量的情况下发出的附加字节代码命令所需的时间。

例如,如果你有一个更密集的形式函数:

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 ,我们可以看到它们之间的字节代码差异。对于默认版本,我们可以看到基本上发出了一个命令(在两种情况下都忽略POP_TOP用于函数调用 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

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

< 256

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

即使这些命令真的很快,但它们总结起来了。越多的参数越大,并且当实际执行对函数的许多调用时,这些参数堆积起来导致执行时间的感觉差异。

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

  1. 位置参数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 需要4个额外命令来加载每个值。
  2. 列出解压缩foo(10, 20, 30, 40) :4 foo(*[10, 20, 30, 40])个命令以及其他 BUILD_LIST 命令。
    • 使用LOAD_CONST中的列表会减少执行,因为我们提供了一个包含值的已构建列表。
  3. 字典解压缩foo(*l) :8 foo(**{'a':10, 'b':20, 'c': 30, 'd': 40})个命令和 BUILD_MAP
    • 与列表解包一样LOAD_CONST将减少执行,因为将提供构建列表。
  4. 总而言之,不同呼叫情况的执行时间顺序为:

    foo(**d)

    我建议对这些案例使用defaults < positionals < keyword arguments < list unpacking < dictionary unpacking 并查看其差异。

    结论:

    正如@goofd在评论中指出的那样,这确实是一个不应该担心的事情,它确实取决于用例。如果你经常打电话给&#39; light&#39;从计算的角度来看,指定默认值会略微提高速度。如果您经常提供不同的值,则几乎不会产生任何值。

    所以,它可能是微不足道的,试图从像这样的模糊边缘案件中获得提升,这实际上是在推动它。如果您发现自己这样做,可能需要查看PyPyCython等内容。