Python是否优化lambda x: x

3 投票
4 回答
2768 浏览
提问于 2025-04-17 21:34

我写了一段代码来找最大值和最小值,假设生成数据的过程比较慢(否则我就直接用 maxmin 了),这段代码需要一个关键函数,如果没有提供,就会使用一个默认的函数:

if key is None:
    key = lambda x: x

然后在后面:

for i in iterable:
    key_i = key(i)

由于生成数据的过程比较慢,这个问题可能没什么意义,但如果没有提供关键函数,我会对每个项目调用 lambda x: x我想Python可能会优化掉这个默认函数。有人能告诉我它是否真的这样做了吗? 如果没有优化,那这个过程会消耗多少资源?有没有更好的方法来实现这个功能,而不需要增加太多代码行数(比如使用三元运算符)?

4 个回答

1

CPython并不会把这个优化掉。为什么呢?因为在调用之前,它并不知道这个函数是个恒等函数。

3

很遗憾,lambda x: x 只是创建了一个函数,从外面看我们根本不知道它在干什么。当然,从理论上讲,我们可以意识到这只是一个恒等函数,计算起来其实没什么意义。但即便如此,我们也只是把这个函数存储在一个变量里,暂时就这样吧。

然后稍后我们调用这个名字,执行底层的函数。因为这是一个函数,而我们对这个函数一无所知,所以我们只能执行它。理论上,一个优化器可以识别出这是一个恒等函数,直接返回值而跳过调用,但在Python中做到这一点会很困难。Peephole优化器在看到某些可能性时,已经会去掉一些字节码指令,但在这种情况下,这就很难实现:

调用一个名字通常是一个 LOAD_FAST 操作,接着是加载参数,然后是 CALL_FUNCTION。这直接来自于 something(args) 的语法。所以一个理论上的优化器需要跳过第一次加载和调用函数。但要做到这一点,它必须知道最开始加载的名字指的是一个恒等函数。

现在,Peephole优化器的工作方式是,它不处理动态变量内容。即使我们能给函数加上某种标记,以便快速检查它是否是恒等函数,优化器仍然无法读取这些信息,因为它不处理底层数据。它只处理字节码操作,比如把 LOAD_GLOBAL True 简化为 LOAD_CONST True

说实话,为恒等函数引入这样的标记其实挺奇怪的。恒等函数本身就很少见;如果我们要优化这个,完全可以把所有的lambda函数内联,这样就能完全减少函数调用的开销。但这并不是Peephole优化器或任何其他解释型语言的优化器所做的。运行时的开销可能会太大,反而对整体性能产生负面影响,根本不值得进行这种微优化。

因为大多数情况下,这种级别的优化根本不值得。这样的函数调用很少会成为你应用的瓶颈,即使真的是,你也得考虑用其他方式来优化它。

4

性能检查

这里还没有人做过性能检查,所以我想分享一下我认为可能更好的选择:使用三元运算符或者分开控制流程(我觉得分开控制流程会是最好的解决方案)。

为了测试它们,我们可以在 Python 3 中这样做:

import timeit

setup = """
def control_flow(iterable, key=None):
    if key is None:
        for i in iterable:
            pass
    else:
        for i in iterable:
            key_i = key(i)

def identity_lambda(iterable, key=None):
    if key is None:
        key = lambda x: x
    for i in iterable:
        key_i = key(i)

def ternary(iterable, key=None):
    for i in iterable:
        key_i = key(i) if key else i
"""
print('Testing no lambda')
timeit.timeit('control_flow(range(100))', setup=setup)
timeit.timeit('identity_lambda(range(100))', setup=setup)
timeit.timeit('ternary(range(100))', setup=setup)
print('Testing with lambda')
timeit.timeit('control_flow(range(100), lambda x: -x)', setup=setup)
timeit.timeit('identity_lambda(range(100), lambda x: -x)', setup=setup)
timeit.timeit('ternary(range(100), lambda x: -x)', setup=setup)

这是测试结果:

Testing no lambda
1.8421741100028157
10.212458187001175
3.39080909700715

Testing with lambda
14.262093641998945
14.405747531011002
14.198169080002117

所以我认为最好的选择是分开控制流程,这样在每个分支下的代码量基本上翻倍,而不是使用 lambda x: x,至少在这种情况下是这样。我认为这里最重要的一点是,Python 对于身份函数并没有进行优化,有时候增加代码行数反而能提高性能,尽管这样可能会导致代码不易维护和更容易出错。

9

好问题!一个优化器可能会发现,在某些可预测的情况下,foo 可能是一个恒等函数,然后就会创建一个替代路径,用已知的结果来替代它的调用。

我们来看一下操作码:

>>> def foo(n):
...     f = lambda x:x
...     return f(n)
... 
>>> import dis
>>> dis.dis(foo)
  2           0 LOAD_CONST               1 (<code object <lambda> at 0x7f177ade7608, file "<stdin>", line 2>) 
              3 MAKE_FUNCTION            0 
              6 STORE_FAST               1 (f) 

  3           9 LOAD_FAST                1 (f) 
             12 LOAD_FAST                0 (n) 
             15 CALL_FUNCTION            1 
             18 RETURN_VALUE         

CPython(测试了2.7和3.3版本)似乎并没有优化掉这个lambda调用。也许其他实现会这样做?

>>> dis.dis(lambda x:x)
  1           0 LOAD_FAST                0 (x) 
              3 RETURN_VALUE   

恒等函数其实没做什么。所以每次你调用这个恒等函数时,基本上需要优化掉2个LOAD_FAST、1个CALL_FUNCTION和1个RETURN_VALUE,而创建一个可靠的替代路径(就像@viraptor说的,可能比看起来更复杂)则是另一回事。

也许在Python代码中使用else路径会更好。

你在最小/最大示例中真正的优化是通过存储结果来减少函数的调用次数。现在它只被调用n次,而不是n*4,这样的确是个不错的提升!

撰写回答