优化Python中常量的冗余逻辑测试

时间:2013-02-12 23:54:45

标签: python optimization refactoring

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

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

实现1)和2)之间的计算效率是否有任何差异?换句话说,Python是否进行任何自动优化以使这两个实现在机器代码级别上等效?

2 个答案:

答案 0 :(得分:2)

虽然@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

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

答案 1 :(得分:1)

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

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上64位CPython 3.3.0。

但是,如果您对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(Mac上的所有64位版本,然后只使用Cython 0.17.1 pyximport(在Python 3.3.0下)编译与Cython代码相同的源:< / p>

                 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.1x到3x之间的速度;另外从循环中提取比较可能会给你另外3%左右 - 但与使用PyPy而不是CPython,Cython而不是Python,甚至3.x而不是2.x相比,所有这些都没有。编写实际的Cython或自定义C代码,或将循环移动到numpy,会更快。

如果你考虑一下,这是有道理的。如果10亿布尔比较或全局查找的成本很重要,十亿次函数调用的成本和通过解释器的十亿次循环将更加重要。如果你不打算优化它(你通常可以通过使用生成器表达式,列表理解,map调用等来代替循环,即使切换解释器,也可以重写代码在numpy等不可行的情况下,你不应该担心这些小东西。

显然,如果最后3%确实有所作为,那么你需要在你真正关心的平台上进行更实际的测试。

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