为什么这个生成器表达式函数比循环版本慢?

11

我一直遵循这样的理论,即生成器表达式比普通循环更高效。但是,我遇到了以下例子:编写一个函数,该函数给定一个数字N和一些因子ps,返回小于N且至少是一个因子的倍数的所有数字的总和。

以下是使用循环和较短的生成器表达式的两种版本:

def loops(N, ps):
    total_sum = 0 
    for i in xrange(N):
        for p in ps: 
            if i%p == 0:
                total_sum += i
                break
    return total_sum

def genexp(N, ps):
    return sum(i for i in xrange(N)
               if any(i%p == 0 for p in ps))

我期望这两个表现大致相同,也许理解版会更快一些,但是我没有预料到的是:

for func in ('loops', 'genexp'):
    print func, timeit.timeit('%s(100000, [3,5,7])' % func, 
                              number=100, 
                              setup='from __main__ import %s' % func)


loops 2.82878184319
genexp 10.1663100719

4倍慢甚至不接近!为什么?我误解了什么吗?


你使用的是生成器表达式,而不是列表推导式。 - Martijn Pieters
@MartijnPieters 谢谢!显然我不是 Python 方面的行家 :) - Barry
1个回答

11

首先,生成器表达式在内存效率上是高效的,但不一定快速。

你精简的genexp()版本之所以较慢,有两个原因:

  • 生成器表达式使用新作用域(就像新函数)。你正在产生N个新作用域,每个any()测试都需要一个。创建和销毁新作用域的成本相对较高,尤其是在循环中执行时,并与不执行这些操作的代码进行比较。

  • sum()any()名称是要查找的额外全局变量。在any()的情况下,每个测试都需要额外的N次全局查找。必须在字典中查找全局变量,而本地变量则通过C数组索引查找(非常快速)。

后者只是一个小组成部分,大部分代价在于创建和销毁帧(作用域);如果你创建一个版本,其中_any_sum是函数的本地变量,性能会略微提高:

>>> def genexp_locals(N, ps, _any=any, _sum=sum):
...     return _sum(i for i in xrange(N)
...                if _any(i%p == 0 for p in ps))
... 
>>> for func in ('loops', 'genexp', 'genexp_locals'):
...     print func, timeit.timeit('%s(100000, [3,5,7])' % func, 
...                               number=100, 
...                               setup='from __main__ import %s' % func)
... 
loops 2.00835800171
genexp 6.45241594315
genexp_locals 6.23843789101

我没有为xrange创建一个本地变量,以保持这一方面的一致性。从技术上讲,_any名称被生成器表达式代码对象作为闭包而非本地变量进行查找,这种方式不像全局查找那样慢,但也不如本地查找快。


我认为“每个any()测试都有N个新作用域”应该改为“每个any()测试都有一个新作用域,共N个”。而且我认为还有另一个成本是你没有提到的,那就是生成器对象和它们的消费者之间来回切换的成本。 - Manuel
@Manuel:与所有其他开销相比,那种开销可以说是微不足道的。感谢您对措辞的建议,我已经采纳了。 - Martijn Pieters
你的意思是总体来说可以忽略不计,还是仅针对这种情况(因为这些发电机几乎没有产生任何东西)? - Manuel
@Manuel:在这种特定情况下。 - Martijn Pieters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接