gpt4 book ai didi

python - `functools.partial` 到底在做什么优化?

转载 作者:太空狗 更新时间:2023-10-29 17:32:36 29 4
gpt4 key购买 nike

CPython 3.6.4:

from functools import partial

def add(x, y, z, a):
return x + y + z + a

list_of_as = list(range(10000))

def max1():
return max(list_of_as , key=lambda a: add(10, 20, 30, a))

def max2():
return max(list_of_as , key=partial(add, 10, 20, 30))

现在:

In [2]: %timeit max1()
4.36 ms ± 42.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [3]: %timeit max2()
3.67 ms ± 25.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

我以为 partial 只是记住部分参数,然后在调用其余参数时将它们转发给原始函数(所以它只不过是一个快捷方式),但它似乎使一些优化。在我的例子中,整个 max2 函数与 max1 相比优化了 15%,这非常好。

如果知道优化是什么,那就太好了,这样我就可以更有效地使用它。 Docs对任何优化保持沉默。毫不奇怪,“大致相当于”实现(在文档中给出)根本没有优化:

In [3]: %timeit max2()  # using `partial` implementation from docs 
10.7 ms ± 267 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

最佳答案

以下参数实际上仅适用于 CPython,对于其他 Python 实现可能完全不同。你实际上说你的问题是关于 CPython 但我认为重要的是要意识到这些深入的问题几乎总是取决于实现细节,这些细节可能因不同的实现而不同,甚至可能在不同的 CPython 版本之间有所不同(例如 CPython 2.7 可能完全不同,但 CPython 3.5 也可能如此)!

时间

首先,我无法重现 15% 甚至 20% 的差异。在我的电脑上,差异约为 10%。当您更改 lambda 时它甚至更少,因此它不必从全局范围查找 add (正如评论中已经指出的那样,您可以传递 添加 函数作为函数的默认参数,以便在本地范围内进行查找。

from functools import partial

def add(x, y, z, a):
return x + y + z + a

def max_lambda_default(lst):
return max(lst , key=lambda a, add=add: add(10, 20, 30, a))

def max_lambda(lst):
return max(lst , key=lambda a: add(10, 20, 30, a))

def max_partial(lst):
return max(lst , key=partial(add, 10, 20, 30))

我实际上对这些进行了基准测试:

enter image description here

from simple_benchmark import benchmark
from collections import OrderedDict

arguments = OrderedDict((2**i, list(range(2**i))) for i in range(1, 20))
b = benchmark([max_lambda_default, max_lambda, max_partial], arguments, "list size")

%matplotlib notebook
b.plot_difference_percentage(relative_to=max_partial)

可能的解释

很难找到差异的确切原因。然而,有几个可能的选择,假设你有一个 CPython version with compiled _functools module (我使用的所有桌面版 CPython 都有它)。

正如您已经发现的 Python version of partial会明显变慢。

  • partial 在 C 中实现,可以直接调用函数 - 无需中间 Python 层1。另一方面,lambda 需要对“捕获的”函数进行 Python 级调用。

  • partial 实际上知道参数如何组合在一起。因此它可以更有效地创建传递给函数的参数(它只是 concatenats the stored argument tuple to the passed in argument tuple )而不是构建一个全新的参数元组。

  • 在最近的 Python 版本中,为了优化函数调用(所谓的 FASTCALL 优化),更改了几个内部结构。 Victor Stinner 在他的 blog 上有一个相关的拉取请求列表。如果您想了解更多信息。

    这可能会影响 lambdapartial 但同样是因为 partial 是一个 C 函数,它知道 无需像 lambda 那样推断直接调用哪个。

然而,认识到创建 partial 有一些开销是非常重要的。收支平衡点是 ~10 个列表元素,如果列表更短,则 lambda 会更快。

脚注

1 如果您从 Python 调用一个函数,它使用操作代码 CALL_FUNCTION,它实际上是一个 wrapper (that's what I meant with Python layer) around the PyObject_Call* (or FASTCAL) functions .但它还包括创建参数元组/字典。如果您从 C 函数调用函数,您可以通过直接调用 PyObject_Call* 函数来避免这种薄包装。

如果您对操作码感兴趣,可以disassemble the function :

import dis

dis.dis(max_lambda_default)

0 LOAD_GLOBAL 0 (max)
2 LOAD_FAST 0 (lst)
4 LOAD_GLOBAL 1 (add)
6 BUILD_TUPLE 1
8 LOAD_CONST 1 (<code object <lambda>>)
10 LOAD_CONST 2 ('max_lambda_default.<locals>.<lambda>')
12 MAKE_FUNCTION 1 (defaults)
14 LOAD_CONST 3 (('key',))
16 CALL_FUNCTION_KW 2
18 RETURN_VALUE

Disassembly of <code object <lambda>>:
0 LOAD_FAST 1 (add) <--- (2)
2 LOAD_CONST 1 (10)
4 LOAD_CONST 2 (20)
6 LOAD_CONST 3 (30)
8 LOAD_FAST 0 (a)
10 CALL_FUNCTION 4 <--- (1)
12 RETURN_VALUE

如您所见,CALL_FUNCTION 操作码 (1) 实际上就在那里。

顺便说一句:LOAD_FAST (2) 负责 lambda_default 和没有默认的 lambda 之间的性能差异(有求助于较慢的查找)。这是因为加载名称实际上是从检查局部作用域(函数作用域)开始的,在 add=add 的情况下,add 函数在局部作用域中,因此它可以进行更快的查找。如果您在本地范围内没有它,它将检查每个周围的范围,直到找到名称,并且只有在到达全局范围时才会停止。每次调用 lambda 时都会完成该查找!

关于python - `functools.partial` 到底在做什么优化?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49965774/

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