为啥 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】:

配方更快的原因是它的关键部分(islicedeque)是用 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 来完成的,除了forxrange 上的循环使成本大约翻倍。

【讨论】:

我在纯 python 中实现 islice,仍然得到与我在问题中解释的结果相同的结果,所以这不是原因 @qwwqwwq 你是怎么计时的?我想您要么使用少量数据,因此执行此操作所需的时间实际上并不重要,而且开销恰好更高,或者您的时间错误。 @qwwqwwq:您的纯 Python islice 实现无法正常工作。当startstop 都相同时(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 使用效率低下

为啥 itertools.groupby() 不起作用? [复制]

为啥python线程会消耗这么多内存?

为啥标签与 pandas、itertools 和 numpy 索引不一致?