【问题标题】:Optimizing redundant logical tests on constants in Python优化 Python 中常量的冗余逻辑测试
【发布时间】:2013-02-12 23:54:44
【问题描述】:

假设我有一个主函数,它执行布尔逻辑测试来决定运行 2 个子函数中的一个,函数 A 或函数 B。主函数循环了 10 亿次,但逻辑测试的值是常量(由用户在程序启动时输入)。

我看到了 2 种可能的写法: 1)将逻辑测试埋在函数A中。至少在理论上,逻辑测试必须执行10亿次,这听起来效率不高。 2)在main函数之前做逻辑测试。将主函数拆分为主函数 1 和主函数 2(除了运行哪个子函数之外,它们是相同的),并使用逻辑测试来决定运行哪个主函数。在这里,逻辑测试只执行一次,但是这种实现会产生冗余代码。

实现 1) 和 2) 的计算效率有什么不同吗?换句话说,Python 是否会进行任何自动优化以使这两种实现在机器码级别等效?

【问题讨论】:

  • 我建议你可以自己计时:docs.python.org/2/library/timeit.html
  • Python 没有做任何你想的优化。此外,如果十亿个简单的 if 语句会导致您的代码变慢,那么您就没有做任何有趣的事情。

标签: python optimization refactoring


【解决方案1】:

虽然@mmgp 在这两个方面都是正确的——CPython 不做任何这样的优化,这不太可能成为 Python 擅长的代码类型的瓶颈——还有第三种选择。您可以将要使用的函数作为参数传递:

>>> def g1():
...         print 'g1'
...     
>>> def g2():
...         print 'g2'
...     
>>> def subfunc(fn):
...         fn()
...     
>>> def caller(a):
...         f = g1 if a else g2
...         for i in range(2):
...                 subfunc(f)
...         
>>> caller(True)
g1
g1
>>> caller(False)
g2
g2

您的子功能可以保持完全相同,并且您已将测试提升到循环之外。

【讨论】:

  • 这正是我正在寻找的那种“最佳实践”答案。谢谢。
  • 您应该指定,虽然 CPython 不进行任何字节码优化或机器码生成,但 Python 的其他实现(IronPython、Jython、PyPy)使用 JIT 编译器并且可以想象优化此检查在适当的条件下离开。也就是说,传入可调用对象是此处优化的正确方法。
  • 当然,PyPy(也许还有其他人)会如此快速地创建一个微不足道的循环,以至于很难实际测试......
【解决方案2】:

正如 Patashu 建议的那样,让我们​​使用 timeit 进行测试,而不是尝试猜测。我将在ipython 中使用魔法%timeit,因为它更简单。代码如下:

In [275]: def ff(): pass

In [276]: def ft(): pass

In [277]: def f1(b): # naive implementation
   .....:     for i in range(1000000):
   .....:         if b: ft()
   .....:         else: ff()

In [278]: %timeit f1(True)
10 loops, best of 3: 117 ms per loop

In [279]: def f2(b): # DSM's implementation
   .....:     f = ft if b else ff
   .....:     for i in range(1000000):
   .....:         f()

In [280]: %timeit f2(True)
10 loops, best of 3: 99.2 ms per loop

所以,它要快一点,至少在我的 Mac 上的 CPython 3.3.0 64 位中。

但是,如果您对 Python 优化有所了解,您可能会注意到,这与将全局变量移动到局部变量所期望的性能提升大致相同。所以,让我们在不提升布尔表达式的情况下做同样的事情,将其排除在外:

In [277]: def f3(b): # Just local binding, no if hoisting
   .....:     f, g = ft, ff
   .....:     for i in range(1000000):
   .....:         if b: f()
   .....:         else: g()
In [286]: %timeit f3(True)
10 loops, best of 3: 94.8 ms per loop

我整理了一个 more complete test,包括 OP 的预期优化,以及在 3.x 和 2.x 中工作的代码,无需更改,并针对 Apple 2.7.2、python.org 3.3.0、PyPy 1.9 运行它.0/2.7.2 和 Jython 2.5.2(所有 64 位都在 Mac 上构建,然后仅使用 Cython 0.17.1 pyximport(在 Python 3.3.0 下)编译与 Cython 代码相同的源代码:

                 3.3.0   2.7.2   PyPy    Jython  Cython
orig             1.136   1.519   0.091   1.680   0.448
OP optimization  1.119   1.362   0.034   1.613   0.460
rebinding        0.936   1.369   0.030   1.492   0.137
DSM version      0.936   1.329   0.031   1.523   0.138

因此,看起来在循环外绑定名称可以将速度提升 1.1 倍到 3 倍之间;另外,将比较提升到循环之外可能会给你另外 3% 左右的收益——但与使用 PyPy 代替 CPython、Cython 代替 Python,甚至 3.x 代替 2.x 相比,所有这些都微不足道。编写实际的 Cython 或自定义 C 代码,或将循环移至 numpy,会更快。

如果你仔细想想,这是有道理的。如果十亿个布尔比较或全局查找的成本很重要,那么十亿个函数调用和十亿个通过解释器的循环的成本将更加重要。如果您不想优化它(并且您通常可以通过使用生成器表达式、列表理解、map 调用等代替循环来做到这一点,即使切换解释器、围绕 @ 重写代码也是如此987654330@等不可行),你不应该担心小东西。

很明显,如果最后 3% 真的有影响,你需要在你真正关心的平台上执行更真实的测试。

可能值得使用 DSM 的实现,但因为它更惯用且更易于阅读,而不是因为它可能更快,也可能不会更快。

【讨论】:

  • 你会注意到我没有使用“优化”这个词来指代我的回答。 :^) 我能想到担心这一点的唯一原因是它是否使代码更清晰。任何比pass 复杂得多的东西,我希望任何(理论上的)好处都会在噪音中消失。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-04
  • 1970-01-01
  • 2013-09-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多