Python 中位置参数和关键字参数之间的性能差异?

Gri*_*mar 6 python performance arguments

StackOverflow 上有很多关于 Python 函数的位置参数和关键字参数之间差异的解释,而且实用性上的差异对我来说很清楚。但是,我无法找到明确的答案(或许还可以解释)在某些情况下位置参数是否比关键字参数快得多。

一方面,我可以想象它们会是这样,因为不需要做任何工作来查找通过的参数。但另一方面,除非参数作为dict扩展传递到**kwargs, 或类似的东西中,否则我想编译器可能会优化这个问题,并且字节码对于基本情况可能执行几乎相同的操作。

所以:

from timeit import timeit


def fun1(a, b, c):
    pass  # or some operation that wouldn't get this function optimised out altogether


def fun2(a, /, b, c=3):
    pass  # or some operation that wouldn't get this function optimised out altogether


fun1(1, 2, 3)  # call 1
fun2(1, 2, 3)  # call 2
fun2(1, 2, c=3)  # call 3
fun2(1, b=2, c=3)  # call 4
d = {'c': 3}
fun2(1, 2, *d)  # call 5
Run Code Online (Sandbox Code Playgroud)

我的问题是:哪些调用比其他调用慢,为什么?我是否错过了一个关键示例,该示例将重点关注此处最差的性能打击?

我认为调用 5 显然是最糟糕的,因为 Python 需要在运行时评估已传递的参数。但这取决于优化是否更有效地处理调用 4 和 3。为了使调用 1 比调用 2 更快,必须进行很少的优化或根本不需要优化。

我知道答案可能取决于特定的 Python 编译器/VM 实现,因此我特别询问大多数人将使用的默认 Python 解释器。

当然,人们可以编写一个脚本来测量差异,但我觉得无论函数做什么,也许还有一些本地情况,这都会很快变得混乱,所以我希望有人能读到这个问题,并真正知道为什么会有两者之间存在差异。

确实运行了计时器测试,它证实了我的怀疑:

print(timeit(lambda: fun1(1, 2, 3), number=int(1e8)))  # call 1
print(timeit(lambda: fun2(1, 2, 3), number=int(1e8)))  # call 2
print(timeit(lambda: fun2(1, 2, c=3), number=int(1e8)))  # call 3
print(timeit(lambda: fun2(1, b=2, c=3), number=int(1e8)))  # call 4
d = {'c': 3}
print(timeit(lambda: fun2(1, 2, *d), number=int(1e8)))  # call 5
Run Code Online (Sandbox Code Playgroud)
7.7741496
8.472946400000001
9.2335016
9.755567000000003
18.6909664
Run Code Online (Sandbox Code Playgroud)

(请注意,我进行了额外的运行,随机化这些函数的调用顺序,并且相对结果或多或少保持相同)

我自己的推理:由于Python必须进行大量分析才能知道在整个程序运行过程中如何调用函数,并且某些调用可能只能有条件地执行,因此它无法真正优化函数的调用方式。因此,关键字参数总是会受到惩罚,并且将关键字参数与关键字一起使用会增加惩罚。

正如评论中所指出的:如果您足够关心性能,这确实很重要,您可能需要重新考虑在标准 VM 上选择 Python 作为平台。无论如何,痴迷于这种类型的性能问题往往还为时过早。然而,问题并不是“我如何优化”或“我应该优化”,而是“为什么实际上存在性能差异”?

感谢您在答案中的任何评论或解释。