【问题标题】:Why do argument-less function calls execute faster?为什么无参数函数调用执行得更快?
【发布时间】:2016-01-04 17:43:50
【问题描述】:

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


为什么会这样?我一般应该尽量避免在调用期间指定参数值吗?

【问题讨论】:

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


    【解决方案1】:

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

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

    例如,如果你有一个更密集的表单函数:

    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
    

    另一方面,在使用关键字的情况下,再发出 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) 将减少执行,因为将提供已构建的列表。

    总而言之,不同调用案例的执行时间排序为:

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

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


    总结:

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

    因此,它可能可以忽略不计,并且试图从像这样的模糊边缘案例中获得提升确实是在推动它。如果您发现自己正在这样做,您可能需要查看 PyPyCython 之类的内容。

    【讨论】:

    • 好答案。但是有两件事 1) 理想情况下,您应该为函数声明 + 使用计时。默认需要更多时间来初始化。 2)它也与用例有关。如果您要在大部分时间使用默认值,最好声明默认值。但是,如果例如a 可以取多个值,为 a 声明默认值是没用的
    • 如果您不为fooDefaults 提供参数或值,那么与fooKwargs 相比,它实际上并没有做同样的事情;所以你可能根本没有任何参数,只做return 10 * 20 + 30 * 40
    • 感谢您的反馈。 @goofd 我将编辑第二点并稍后添加它,因为这里的用例真的很重要。
    • 有趣的是,在我的机器上,使用位置参数调用的无默认函数与仅使用默认值调用的默认函数一样快。
    猜你喜欢
    • 2019-06-18
    • 2013-05-17
    • 1970-01-01
    • 1970-01-01
    • 2012-06-29
    相关资源
    最近更新 更多