【问题标题】:Speed difference between iterating over generators and lists迭代生成器和列表之间的速度差异
【发布时间】:2013-09-10 00:25:22
【问题描述】:

在下面的简单示例中,有两个函数可以对随机数列表进行排序。第一种方法传递sorted一个生成器表达式,第二种方法先创建一个列表:

import random
l = [int(1000*random.random()) for i in xrange(10*6)]

def sort_with_generator():
    return sorted(a for a in l)

def sort_with_list():
    return sorted([a for a in l])

使用line profiler 进行基准测试表明第二个选项 (sort_with_list) 的速度大约是生成器表达式的两倍。

谁能解释发生了什么,为什么第一种方法比第二种慢很多?

【问题讨论】:

  • 您是否在列表示例中的每个元素上加 1?
  • 我很茫然。您可以将两者隔离并分别进行基准测试吗?也许解释器正在对列表进行一些智能缓存或类似的奇怪事情。
  • 列表理解一次在内存中创建整个列表,而生成器表达式通过传递给排序函数的元组提供结果序列的每个元素。因此,列表理解更快,但它消耗更多的内存。生成器表达式速度较慢,但​​在任何给定时间仅为列表的一个元素保留内存。有关更多信息,请查看此问题:stackoverflow.com/questions/47789/…
  • @elyase 道歉,在粘贴过程中偷偷进入 - 不,除了表达式之外,它们应该是相同的。
  • 问题可以简化为list(a for a in l) vs. [a for a in l]。这就是差异的来源。后者的速度与使用 sorted 时相同。

标签: python performance generator


【解决方案1】:

您的第一个示例是迭代列表的生成器表达式。您的第二个示例是迭代列表的列表表达式。确实,第二个例子稍微快了一点。

>>> import timeit
>>> timeit("sorted(a for a in l)", setup="import random;l = [int(1000*random.random()) for i in xrange(10*6)]")
5.963912010192871
>>> timeit("sorted([a for a in l])", setup="import random;l = [int(1000*random.random()) for i in xrange(10*6)]")
5.021576881408691

毫无疑问,这是因为创建列表是一次性完成的,而迭代生成器需要函数调用。

生成器不会加速这样的小列表(列表中有 60 个元素,这非常小)。主要是为了在创建长列表时节省内存。

【讨论】:

  • 翻转这两个,告诉我你是否看到生成器更快。我还假设它做了一个a+1
  • 在这种情况下两者是隔离的,因为每个都有l的单独初始化。我怀疑我们会观察到同样的现象。
  • 其实我的代码应该已经初始化了10**6items =D。似乎它们在10**510**5 之间的某个地方实现了收支平衡。我仍然不确定我明白为什么。
  • @Brian:嗯?如果你们两个想说的是,如果我将随机数的生成直接移到 sorted() 调用中,它会改变结果:我在发布答案之前尝试过,只是为了确保。正如我所怀疑的那样,它没有。
  • 不,不是那样。我们建议对单个列表进行两次连续迭代可能会由于某种缓存等而引入性能差异。我想得越多,找到它的可能性就越小
【解决方案2】:

如果您在the source 中查看sorted,则您传入的任何序列都会首先被复制到一个新列表中。

newlist = PySequence_List(seq);

generator --> list 似乎比 list 慢 --> list

>>> timeit.timeit('x = list(l)', setup = 'l = xrange(1000)')
16.656711101531982
>>> timeit.timeit('x = list(l)', setup = 'l = range(1000)')
4.525658845901489

至于为什么必须制作副本,请考虑排序的工作原理。排序不是线性算法。我们多次遍历数据,有时会双向遍历数据。生成器旨在生成一个序列,我们从开始到之后的某个地方迭代一次且仅一次。列表允许随机访问。

另一方面,从生成器创建列表意味着内存中只有一个列表,而制作列表的副本意味着内存中有两个列表。很好的老式时空权衡。

Python 使用Timsort,这是一种合并排序和插入排序的混合体。

【讨论】:

  • 不,生成器 --> list 并不比 list --> list 慢。但是,它可能比首先生成列表然后将其复制到列表中要慢。所以无论如何都要 +1。
【解决方案3】:

列表表达式首先将数据加载到内存中。然后对结果列表进行任何操作。让分配时间为T2(对于第二种情况)。 生成器表达式不会立即分配时间,但它会更改时间 t1[i] 的迭代器值。所有t1[i] 的总和将是T1T1T2.

但是当您调用sorted() 时,在第一种情况下,T1 与排序相比添加了每对的分配内存时间(tx1[i])。结果,T1 与所有tx1[i] 的总和相加。

因此,T2 T1 + sum(tx1[i])

【讨论】:

  • sorted 没有分配“每对比较的内存”,所以这没什么意义。对于需要大量内存的巨大列表。使用生成器进行排序可能效率较低,但不是这个原因。
  • 那么,那你如何解释生成器表达式在迭代时没有存储在所有先前值的内存中?那他们怎么排序呢?
  • 它显然存储了它排序的值,是的。对,没有。由于在这种情况下没有 key 或 cmp 函数,所以它存储的是它排序的列表。
猜你喜欢
  • 2019-06-06
  • 2021-12-27
  • 2021-11-06
  • 2017-02-06
  • 2017-04-09
  • 1970-01-01
  • 1970-01-01
  • 2017-12-24
  • 2010-09-20
相关资源
最近更新 更多