假设我有一个执行布尔逻辑测试的主函数来决定运行2个子函数中的1个,函数A或函数B.主函数循环10亿次,但逻辑测试的值是a常量(在程序启动时由用户输入)。
我看到有两种可能的方式来写这个: 1)将逻辑测试归入功能A.至少在理论上,逻辑测试必须执行10亿次,这听起来效率不高。 2)在主函数之前进行逻辑测试。将主函数拆分为主函数1和主函数2(除了它们运行的子函数之外是相同的),并使用逻辑测试来决定运行哪个主函数。在这里,逻辑测试只执行一次,但是这种实现会创建冗余代码。
实现1)和2)之间的计算效率是否有任何差异?换句话说,Python是否进行任何自动优化以使这两个实现在机器代码级别上等效?
答案 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的实现 - 但因为它更惯用且更容易阅读,而不是因为它可能会或可能不会更快。