为啥 python itertools “消耗”配方比调用下 n 次更快?
Posted
技术标签:
【中文标题】为啥 python itertools “消耗”配方比调用下 n 次更快?【英文标题】:Why is python itertools "consume" recipe faster than calling next n times?为什么 python itertools “消耗”配方比调用下 n 次更快? 【发布时间】:2013-05-18 23:04:42 【问题描述】:在 itertools 的 Python 文档中,它提供了以下用于推进迭代器 n 步的“配方”:
def consume(iterator, n):
"Advance the iterator n-steps ahead. If n is none, consume entirely."
# Use functions that consume iterators at C speed.
if n is None:
# feed the entire iterator into a zero-length deque
collections.deque(iterator, maxlen=0)
else:
# advance to the empty slice starting at position n
next(islice(iterator, n, n), None)
我想知道为什么这个配方与这样的东西根本不同(除了消耗整个迭代器的处理):
def other_consume(iterable, n):
for i in xrange(n):
next(iterable, None)
我使用timeit
确认,正如预期的那样,上述方法要慢得多。允许这种卓越性能的配方中发生了什么?我知道它使用islice
,但看看islice
,它似乎在做与上面的代码基本相同的事情:
def islice(iterable, *args):
s = slice(*args)
it = iter(xrange(s.start or 0, s.stop or sys.maxint, s.step or 1))
nexti = next(it)
### it seems as if this loop yields from the iterable n times via enumerate
### how is this different from calling next n times?
for i, element in enumerate(iterable):
if i == nexti:
yield element
nexti = next(it)
注意:即使我没有从itertools
导入islice
,而是使用上面显示的文档中的python 等效项来定义它,但配方仍然更快..
编辑:timeit
此处代码:
timeit.timeit('a = iter([random() for i in xrange(1000000)]); consume(a, 1000000)', setup="from __main__ import consume,random", number=10)
timeit.timeit('a = iter([random() for i in xrange(1000000)]); other_consume(a, 1000000)', setup="from __main__ import other_consume,random", number=10)
other_consume
每次运行都会慢 2.5 倍
【问题讨论】:
【参考方案1】:配方更快的原因是它的关键部分(islice
、deque
)是用 C 实现的,而不是用纯 Python 实现的。部分原因是 C 循环比 for i in xrange(n)
快。另一部分是 Python 函数调用(例如next()
)比它们的 C 等价物更昂贵。
您从文档中复制的itertools.islice
的版本不正确,而且它的出色性能显然是因为使用它的消费函数不会消费任何东西。 (出于这个原因,我没有在下面显示该版本的测试结果,尽管它非常快!:)
这里有几个不同的实现,所以我们可以测试什么是最快的:
import collections
from itertools import islice
# this is the official recipe
def consume_itertools(iterator, n):
"Advance the iterator n-steps ahead. If n is none, consume entirely."
# Use functions that consume iterators at C speed.
if n is None:
# feed the entire iterator into a zero-length deque
collections.deque(iterator, maxlen=0)
else:
# advance to the empty slice starting at position n
next(islice(iterator, n, n), None)
# your initial version, using a for loop on a range
def consume_qwwqwwq(iterator, n):
for i in xrange(n):
next(iterator, None)
# a slightly better version, that only has a single loop:
def consume_blckknght(iterator, n):
if n <= 0:
return
for i, v in enumerate(iterator, start=1):
if i == n:
break
我的系统上的时间(Windows 7 上的 Python 2.7.3 64 位):
>>> test = 'consume(iter(xrange(100000)), 1000)'
>>> timeit.timeit(test, 'from consume import consume_itertools as consume')
7.623556181657534
>>> timeit.timeit(test, 'from consume import consume_qwwqwwq as consume')
106.8907442334584
>>> timeit.timeit(test, 'from consume import consume_blckknght as consume')
56.81081856366518
我的评估是,一个几乎空的 Python 循环的运行时间比 C 中的等效循环长七到八倍。一次循环两个序列(如 consume_qwwqwwq
通过在 iterator
上调用 next 来完成的,除了for
xrange
上的循环使成本大约翻倍。
【讨论】:
我在纯 python 中实现 islice,仍然得到与我在问题中解释的结果相同的结果,所以这不是原因 @qwwqwwq 你是怎么计时的?我想您要么使用少量数据,因此执行此操作所需的时间实际上并不重要,而且开销恰好更高,或者您的时间错误。 @qwwqwwq:您的纯 Pythonislice
实现无法正常工作。当start
和stop
都相同时(next(iter(xrange(100, 100)))
立即引发StopIteration
),它什么也不做。由于这是 consume
函数调用它的方式,因此您的消费不会消费任何东西(但它会很快消费!)。
啊我明白了.. 我直接从文档中复制了 islice 的代码,猜它不正确..?如果你回答我会接受
@qwwqwwq:我已经更新了几个不同的实现,我已经对彼此进行了计时。使用 enumerate
直接循环迭代器的纯 Python 版本比 itertools
配方要长约 7.5 倍。您作为示例提出的简单代码所用的时间几乎是我测试中 itertools
代码的 14 倍。所以,我认为@MartijnPieters 在他的回答中提到的两倍的next
电话是造成你看到的一半减速的原因。剩下的故事是任何 Python 循环都会比 C 中的循环慢。【参考方案2】:
itertools.islice()
上的文档存在缺陷,无法正确处理 start == stop
的边缘情况。这正是consume()
使用的那种边缘情况。
对于islice(it, n, n)
,恰好n
元素从it
被消耗,但没有产生任何结果。相反,StopIteration
在这些 n
元素被消耗后引发。
另一方面,您用来测试的 Python 版本立即引发StopIteration
,而不会消耗来自it
的任何东西。这使得针对这个纯 python 版本的任何计时都不正确并且太快了。
这是因为xrange(n, n, 1)
迭代器立即引发StopIteration
:
>>> it = iter(xrange(1, 1))
>>> print next(it)
Traceback (most recent call last):
File "prog.py", line 4, in <module>
print next(it)
StopIteration
【讨论】:
我认为这只能解释适度的速度差异。使用 naive 版本大约是itertools.islice
配方的 10 倍。用枚举循环替换for i in range(n)
(当索引等于n
时中断)将花费的时间减少到 itertools 版本的 7 倍,但我认为其余部分完全在 C 中具有逻辑。
确实,是文档中的缺陷导致 OP 测试存在缺陷。
确实,我的消费从不消费任何东西!
并为回复速度较慢表示歉意;在 iPhone 上测试和输入所有这些是慢。以上是关于为啥 python itertools “消耗”配方比调用下 n 次更快?的主要内容,如果未能解决你的问题,请参考以下文章
由 itertools.groupby() 生成的迭代器被意外消耗
为啥eclipselink每次重新启动时都会消耗整个allocationSize?
具有多处理功能的 Python itertools - 巨大的列表与使用迭代器的 CPU 使用效率低下