gpt4 book ai didi

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

转载 作者:太空狗 更新时间:2023-10-29 20:12:15 25 4
gpt4 key购买 nike

我设置了一个简单的自定义函数,它接受一些默认参数 (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 我们可以看出它们之间字节码的区别。对于默认版本,我们可以看到基本上发出了一个命令(忽略 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)和值(value)观 (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 在评论中指出的那样,这确实是一个不应该担心的事情,它确实取决于用例。如果您经常从计算的角度调用“light”函数,则指定默认值会略微提高速度。如果您经常提供不同的值,这几乎不会产生任何结果。

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

关于python - 为什么无参数函数调用执行得更快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34596793/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com