【问题标题】:Why this list comprehension is faster than equivalent generator expression?为什么这个列表理解比等效的生成器表达式更快?
【发布时间】:2013-04-24 19:29:30
【问题描述】:

我在 Windows 上使用 Python 3.3.1 64 位,这段代码 sn-p:

len ([None for n in range (1, 1000000) if n%3 == 1])

与这个相比,在 136 毫秒内执行:

sum (1 for n in range (1, 1000000) if n%3 == 1)

在 146 毫秒内执行。在这种情况下,生成器表达式不应该更快或与列表理解的速度相同吗?

我引用 Guido van Rossum From List Comprehensions to Generator Expressions:

...Python 3 中的列表推导和生成器表达式都是 实际上比它们在 Python 2 中更快! (并且不再有 两者之间的速度差异。)

编辑:

我用timeit 测量了时间。我知道这不是很准确,但我只关心这里的相对速度,并且当我使用不同数量的迭代进行测试时,列表理解版本的时间一直在缩短。

【问题讨论】:

  • 你是如何测量速度差异的?
  • 7% 的差异是微不足道的——尤其是如果您的计时不是很准确。 (使用 timeclock 而不是 timeit 的典型幼稚实现只需要 1/8 秒就很容易产生远远大于 7% 的错误。)
  • 您为什么将lensum 进行比较?计数元素比添加它们的内容要快得多。
  • 有点令人惊讶的是,在 PyPy 1.9.0(即 Python 2.7.2,没有任何现代 genexp 改进)中,genexp 版本几乎快了一倍(26.6ms vs. 49.7 毫秒)。加法可能无关紧要(因为在 PyPy 中,整数加法比迭代快几个数量级),但我仍然对结果感到有些惊讶。
  • @MartijnPieters 我使用timeit - 编辑了问题。

标签: python python-3.x list-comprehension generator-expression


【解决方案1】:

我相信这里的区别完全在于增加 1000000 的成本。在 Mac OS X 上使用 64 位 Python.org 3.3.0 进行测试:

In [698]: %timeit len ([None for n in range (1, 1000000) if n%3 == 1])
10 loops, best of 3: 127 ms per loop
In [699]: %timeit sum (1 for n in range (1, 1000000) if n%3 == 1)
10 loops, best of 3: 138 ms per loop
In [700]: %timeit sum ([1 for n in range (1, 1000000) if n%3 == 1])
10 loops, best of 3: 139 ms per loop

所以,并不是理解比 genexp 快;他们都需要大约相同的时间。但是在 list 上调用 len 是即时的,而将 100 万个数字相加会使总时间再增加 7%。

向它抛出几个不同的数字,这似乎可以成立,除非列表非常小(在这种情况下它确实似乎变得更快),或者足够大以至于内存分配开始变得一个重要的因素(目前还没有,333K)。

【讨论】:

  • 这正是我在使用 Python 3.3.1 64bit (Win7) 进行测试时发现的。 +1
  • @TimPietzcker:由于您显然是在我写答案的同时写评论,所以我并不惊讶我们同时运行完全相同的测试。 :)
  • 为了添加数据——使用 Python 3.2 32bit (Win7) 我发现生成器表达式始终慢 2%。微不足道,但可重现。
  • 我不知道listlen 是O(1) - 到目前为止我只用了几天的Python。感谢您指出这一点。
  • @PaulJurczak:实际上很难从文档中挖掘性能保证。但是,如果您知道 list 只是一个可调整大小的数组,并且 [0,1,2][3] 引发了 IndexError 而不是段错误,那么显然它必须将长度保持在某个地方,对吗? (在 CPython 中,它位于 PyVarObject 标头中。)因此,不立即返回它是愚蠢的。
【解决方案2】:

借用this answer,有两点需要考虑:

1. Python 列表是可索引的,获取其长度只需 O(1) 次。这意味着在列表上调用len() 的速度不取决于其大小。但是,如果您在生成器上调用 len(),您将消耗它生成的所有项目,因此时间复杂度为 O(n)。

2. 请参阅上面的链接答案。列表推导是一个紧密的 C 循环,而生成器必须在内部存储对迭代器的引用,并为它生成的每个项目调用 next(iter)。这为生成器创建了另一层开销。在小范围内,可以安全地忽略列表理解和生成器之间的性能差异,但在更大范围内,您必须考虑这一点。

【讨论】:

    猜你喜欢
    • 2020-11-08
    • 1970-01-01
    • 2021-11-12
    • 1970-01-01
    • 2023-02-02
    • 2015-11-29
    • 1970-01-01
    • 2019-09-13
    • 2018-09-12
    相关资源
    最近更新 更多