Python “functools.partial”的优化功能究竟是什么?
CPython 3.6.4:Python “functools.partial”的优化功能究竟是什么?,python,performance,cpython,python-internals,functools,Python,Performance,Cpython,Python Internals,Functools,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=parti
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))
现在:
我认为partial
只记住部分参数,然后在使用其余参数调用时将它们转发到原始函数(因此这只是一个快捷方式),但它似乎进行了一些优化。在我的例子中,整个max2
函数比max1
优化了15%,这非常好
知道优化是什么会很好,这样我就可以更有效地使用它。对于任何优化都保持沉默。毫不奇怪,“大致相当于”实施(在文档中给出)根本没有优化:
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
(正如注释中已经指出的,您可以将add
函数作为默认参数传递给函数,以便在本地范围内进行查找)
实际上,我对这些进行了基准测试:
可能的解释
很难找到造成这种差异的确切原因。不过,假设您有一个(我使用的所有桌面版本的CPython都有它),有一些可能的选择
正如您已经发现的,速度将显著减慢
是用C实现的,可以直接调用函数,而不需要中间的Python层1。另一方面,partial
需要在Python级别调用“captured”函数lambda
实际上知道参数是如何组合在一起的。因此,它可以更有效地创建传递给函数的参数,而不是构建一个全新的参数元组partial
- 在最近的Python版本中,为了优化函数调用(所谓的FASTCALL优化),对一些内部构件进行了更改。Victor Stiner的页面上有一个相关pull请求的列表,以备您进一步了解
这可能会影响
和lambda
,但同样,因为partial
是一个C函数,它知道直接调用哪个函数,而不必像partial
那样推断它lambda
部分
有一些开销。盈亏平衡点是针对约10个列表元素,如果列表较短,则lambda
将更快
脚注
1如果从Python调用函数,它将使用OP codecall_函数
,这实际上是一个。但它还包括创建参数元组/字典。如果从C函数调用函数,则可以通过直接调用PyObject_call*
函数来避免这种薄型包装
如果您对操作码感兴趣,您可以:
导入dis
dis.dis(最大λ默认值)
0加载\u全局0(最大值)
2快速加载0(lst)
4加载_全局1(添加)
6构建元组1
8加载常数1()
10加载常数2(“最大λ默认值”)
12 MAKE_功能1(默认值)
14荷载常数3((‘键’,))
16呼叫功能功率2千瓦
18返回值
反汇编:
0 LOAD_FAST 1(add)不知道partial,谢谢你提醒我。作为一个冒昧的猜测:partial只需要存储一个变量,而不是全部四个变量,因此创建新堆栈帧的开销较小,但这只是一个模糊的(很可能是错误的)猜猜看。我正在等待专业人士深入解释:)如果使用“def”而不是lambda,有什么区别吗?@MichalCharemza我刚刚检查过(创建了一个外部def助手(a):返回add(10,20,30,a)
,并在max
中使用它)速度没有差别。partial
也有C源代码:一个区别是max1
需要在全局范围内为每个调用查找add
,而max2
只查找一次。将add=add
添加为lambda参数将使其更快。或者,您可以将其置于n一个局部变量max1
。不是functools.partial
从Python 3.4开始就在Python中实现了?@PeterNimroot这只是一个后备方法,以防您没有编译的\u functools
模块()。而且很少没有。@mseifer小精度wrt名称解析:LOAD\u name
是因为compile
不知道这里的作用域是什么,如果你dis.dis(lambda:add(10,20,30,a)
)您会发现,函数体使用专门的LOAD\u GLOBAL
和LOAD\u FAST
而不是更通用的LOAD\u NAME
LOAD\u FAST
可以直接索引到帧的本地名称空间,而不必查找和解析名称,从而使其速度特别快,add=add:add(10,20,30,a)
这个add
也是LOAD\u FAST
-ed,这就是为什么这个表单明显更快(这是一个常见的优化)。@masklin这就是我的意思。LOAD\u NAME
使它变慢了-如果是的话会发生什么
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)
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))
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)