【问题标题】:Function closure performance函数闭包性能
【发布时间】:2012-04-02 00:02:44
【问题描述】:

我认为替换这段代码可以提高性能:

def f(a, b):
  return math.sqrt(a) * b
result = []
a = 100
for b in range(1000000):
  result.append(f(a, b))

与:

def g(a):
  def f(b):
    return math.sqrt(a) * b
  return f
result = []
a = 100
func = g(a)
for b in range(1000000):
  result.append(func(b))

我假设由于在执行闭包时a 是固定的,解释器会预先计算涉及a 的所有内容,因此math.sqrt(a) 将只重复一次而不是1000000 次。

我的理解是否总是正确的,或者总是不正确的,或者是正确的/不正确的,取决于实现?

我注意到func 的代码对象是在运行时之前构建的(至少在 CPython 中),并且是不可变的。然后代码对象似乎使用全局环境来实现关闭。这似乎表明我希望的优化没有发生。

【问题讨论】:

  • 实际上该代码不会运行,您调用func 时只有一个参数,尽管它有两个参数。

标签: python performance python-3.x closures implementation


【解决方案1】:

我假设由于在执行闭包时 a 是固定的,解释器会预先计算涉及 a 的所有内容,因此 math.sqrt(a) 只会重复一次,而不是 1000000 次。

这个假设是错误的,我不知道它来自哪里。闭包仅捕获变量绑定,在您的情况下,它捕获a 的值,但这并不意味着正在发生更多魔术:每次调用f 时仍会评估表达式math.sqrt(a)

毕竟,它必须每次都被计算,因为解释器不知道sqrt 是“纯”的(返回值只取决于参数,没有副作用执行)。像您期望的优化在函数式语言中是实用的(引用透明性和静态类型在这里有很大帮助),但在 Python 中很难实现,Python 是一种命令式和动态类型的语言。

也就是说,如果你想预先计算 math.sqrt(a) 的值,你需要明确地这样做:

def g(a):
  s = math.sqrt(a)
  def f(b):
    return s * b
  return f

或者使用lambda:

def g(a): 
  s = math.sqrt(a)
  return lambda b: s * b

既然g 确实返回了一个带有 1 个参数的函数,那么您必须只使用一个参数来调用结果。

【讨论】:

  • 假设只是我自己的想法:嗯,有些东西显然可以优化,所以我猜它必须优化。
  • CPython 不能那样工作。这是一个完全“纯粹”的执行环境,没有任何魔法。
  • 如果不运行 Python 程序,您将无法得知任何有关它的信息。你能做的最好的事情就是猜测。想象一下设置math.sqrt = lambda *args: input("Anything goes") anywhere 会对函数产生什么影响。
  • @max: 非常好的结论 :) 你应该找时间玩 Haskell ;)
  • @3noch 它在字节码中进行某些类型的窥孔优化,例如常量折叠。但通常你说的当然是真的
【解决方案2】:

代码不是静态评估的;函数内部的代码还是每次都计算。函数对象包含表示函数中代码的所有字节码;它不评估任何一个。您可以通过计算一次昂贵的值来改善问题:

def g(a):
    root_a = math.sqrt(a)
    def f(b):
        return root_a * b
    return f
result = []
a = 100
func = g(a)
for b in range(1000000):
    result.append(func(b))

当然,在这个简单的示例中,您可以进一步提高性能:

a = 100
root_a = math.sqrt(a)
result = [root_a * b for b in range(1000000)]

但我认为您正在处理一个比无法扩展的更复杂的示例?

【讨论】:

  • 当然,你完全正确,这只是一个简化的例子。我很好奇 - 这是一种语言要求,它没有被优化掉,还是每个实现都决定它想要做什么?
  • @max:它是特定于实现的,使用 PyPy 或 IronPython 之类的东西可能会得到完全不同的结果(不太确定后者,但它可能会从 .NET IL 优化中受益)。不过,有些事情跨平台是一致的:例如,列表推导很可能比自己构建列表更有效。
【解决方案3】:

像往常一样,timeit 模块是您的朋友。尝试一些事情,看看效果如何。如果您不关心编写丑陋的代码,这可能也会有所帮助:

def g(a):
   def f(b,_local_func=math.sqrt):
      return _local_func(a)*b

显然,python 在尝试访问“全局”变量/函数时会降低性能。如果您可以在本地进行访问,您可以节省一点时间。

【讨论】:

  • 没问题。反正其他的答案都比我的好。我唯一推荐的是考虑使用timeit,如果它确实是代码的性能关键部分。 timeit 非常易于使用,因此绝对值得投入少量时间。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-30
  • 2011-03-14
  • 2022-07-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多