【问题标题】:Speed to iterate multiple times over a generator compared to a list与列表相比,在生成器上多次迭代的速度
【发布时间】:2017-04-09 23:45:21
【问题描述】:

我预计在多个循环的情况下,列表迭代会比使用生成器快得多,而我的代码表明这是错误的。

我的理解是(通过操作我是指定义元素的任何表达式):

  • 一个列表需要初始化 n 个操作
  • 但是列表中的每个循环都只是从内存中抓取一个元素
  • 因此,m 循环遍历列表只需要 n 个操作
  • 生成器不需要初始化任何操作
  • 但是,在生成器上循环运行操作会飞起来
  • 因此,生成器上的一个循环需要 n 次操作
  • 但是 m 在生成器上循环需要 n x m 操作

我使用以下代码检查了我的期望:

from timeit import timeit

def pow2_list(n):
    """Return a list with powers of 2"""

    results = []

    for i in range(n):
        results.append(2**i)

    return results

def pow2_gen(n):
    """Generator of powers of 2"""

    for i in range(n):
        yield 2**i

def loop(iterator, n=1000):
    """Loop n times over iterable object"""

    for _ in range(n):
        for _ in iterator:
            pass

l = pow2_list(1000) # point to a list
g = pow2_gen(1000)  # point to a generator


time_list = \
    timeit("loop(l)", setup="from __main__ import loop, l", number=10)

time_gen = \
    timeit("loop(g)", setup="from __main__ import loop, g", number=10)

print("Loops over list took: ", time_list)
print("Loops over generator took: ", time_gen)

结果让我吃惊...

Loops over list took:  0.20484769299946493
Loops over generator took:  0.0019217690005461918

不知何故,使用生成器似乎比列表快得多,即使循环超过 1000 次也是如此。在这种情况下,我们谈论的是两个数量级!为什么?

编辑:

感谢您的回答。现在我看到了我的错误。我错误地假设生成器从一个新循环开始,比如 range:

>>> x = range(10)
>>> sum(x)
45
>>> sum(x)
45

但这太天真了(范围不是生成器......)。

关于可能的重复评论:我的问题涉及生成器上的多个循环,其他线程中没有解释。

【问题讨论】:

  • 您认为生成器更快的假设是不正确的。 Generators vs List Comprehension performance in Python 的可能重复项
  • 速度差异相差两个数量级应该提醒您注意您的测试有问题。尝试loop(pow_2_gen(1000)) 以获得准确的结果。
  • 你的测试是否有缺陷。一个函数必须在内存中创建一个完整的列表,而另一个必须只返回一个迭代器。建议使用@Dunes 以获得更准确的结果。
  • 我的意见是垃圾问题,因为从未在应用程序源代码中看到此代码。索引点在哪里,谁在没有任何索引的情况下使用这个数组,如何在线性模式下访问 RAM,如何管理 RAM 上的索引索引,为什么这让你感到惊讶?

标签: python performance list iteration generator


【解决方案1】:

您的生成器实际上只循环一次。使用pow2_gen 创建后,g 存储一个生成器;第一次通过loop,这个生成器被消耗,并发出StopIteration。其他时间通过loopnext(g)(或 Python 2 中的g.next())继续抛出StopIteration,因此,实际上g 表示一个空序列。

为了使比较更公平,您需要在每次循环时重新创建生成器。

您处理此方法的另一个困难是您正在调用append 来构建您的列表,这可能是构建列表的最慢方法。更常见的是,列表是使用列表推导式构建的。

下面的代码让我们更仔细地挑选时间。 create_listcreate_gen 分别使用列表理解和生成器表达式创建列表和生成器。 time_loop 就像你的 loop 方法,而 time_applyloop 的一个版本,每次循环都重新创建可迭代对象。

def create_list(n=1000):
    return [2**i for i in range(n)]

def create_gen(n=1000):
    return (2**i for i in range(n))

def time_loop(iterator, n=1000):
    for t in range(n):
        for v in iterator:
            pass

def time_apply(create_fn, fn_arg, n=1000):
    for t in range(n):
        iterator = create_fn(fn_arg)
        time_loop(iterator, 1)

print('time_loop(create_list): %.3f' % timeit("time_loop(create_list(1000))",
                                              setup="from __main__ import *",
                                              number=10))

print('time_loop(create_gen): %.3f' % timeit("time_loop(create_gen(1000))",
                                             setup="from __main__ import *",
                                             number=10))

print('time_apply(create_list): %.3f' % timeit("time_apply(create_list, 1000)",
                                               setup="from __main__ import *",
                                               number=10))

print('time_apply(create_gen): %.3f' % timeit("time_apply(create_gen, 1000)",
                                              setup="from __main__ import *",
                                              number=10))

我的盒子上的结果表明,构建一个列表 (time_apply(create_list)) 在时间上与构建一个生成器 (time_apply(create_gen)) 的时间相似(甚至可能更快)。

time_loop(create_list): 0.244
time_loop(create_gen): 0.028
time_apply(create_list): 21.190
time_apply(create_gen): 21.555

您可以看到您在问题中记录的相同效果,即time_loop(create_gen)time_loop(create_list) 快一个数量级。同样,这是因为创建的生成器仅被迭代一次,而不是列表中的许多循环。

正如您所假设的,在这种特定情况下,构建一个列表并对其进行多次迭代 (time_loop(create_list)) 比对生成器进行多次迭代 (time_apply(create_gen)) 更快。

列表和生成器之间的权衡很大程度上取决于您要创建的迭代器有多大。有 1000 个项目,我希望列表会很快。如果有 100,000 件商品,情况可能会有所不同。

print('create big list: %.3f' % timeit("l = create_list(100000)",
                                       setup="from __main__ import *",
                                       number=10))

print('create big gen: %.3f' % timeit("g = create_gen(100000)",
                                      setup="from __main__ import *",
                                      number=10))

我来了:

create big list: 209.748
create big gen: 0.023

Python 使用了 700 到 800 MB 的内存来构建大列表;生成器几乎不使用任何东西。内存分配和垃圾清理在 Python 中的计算成本很高,并且可以预见地会使您的代码变慢;生成器是一种非常简单的方法,可以避免占用机器的 RAM,并且可以对运行时产生重大影响。

【讨论】:

    【解决方案2】:

    您的测试有问题。也就是说,生成器是不可重复使用的。一旦耗尽,它就不能再次使用,必须生成一个新的。例如。

    l = [0, 1, 2, 4, 5]
    g = iter(l) # creates an iterator (a type of generator) over the list
    
    sum_list0 = sum(l)
    sum_list1 = sum(1)
    assert sum_list0 == sum_list1 # all working normally
    
    sum_gen0 = sum(g) # consumes generator
    sum_gen1 = sum(g) # sum of empty generator is 0
    assert sum_gen0 == sum_list1 # result is correct
    assert sum_gen1 == sum_list1, "second result was incorrect" # because generator was exhausted
    

    为了使您的测试正常工作,您必须在传递给timeit 的语句中重新创建生成器。

    from timeit import timeit
    
    n = 1000
    repeats = 10000
    
    list_powers = [2**i for i in range(n)]
    def gen_powers():
        for i in range(n):
            yield 2**i
    
    time_list = timeit("min(list_powers)", globals=globals(), number=repeats)
    time_gen = timeit("min(gen_powers())", globals=globals(), number=repeats)
    
    print("Loops over list took: ", time_list)
    print("Loops over generator took: ", time_gen)
    

    给予:

    Loops over list took:  0.24689035064701784
    Loops over generator took:  13.551637053904571
    

    现在生成器比列表慢两个数量级。这是可以预料的,因为与序列上的迭代次数相比,序列的大小很小。如果n 很大,则列表创建会变慢。这是因为添加新项目时列表是如何扩展的,并且最终大小在创建时没有传递给列表。与生成器相比,增加迭代次数将加速列表,因为生成器所需的工作量增加,而对于列表,它保持不变。由于n只有1000(小),而repeats支配n,所以生成器比较慢。

    【讨论】:

      【解决方案3】:

      您的测试不起作用,因为您的生成器在loop() 的第一次通过时已耗尽。这是列表相对于生成器的优势之一,您可以对它们进行多次迭代(以将完整列表存储在内存中为代价)。

      这是一个例子。我正在使用生成器表达式和列表理解(这比在 for 循环中使用 append 更优化)但概念是相同的:

      >>> gen = (i for i in range(3))
      >>> for n in range(2):
      ...     for i in gen:
      ...         print(i)
      ... 
      0 # 1st print
      1
      2 # after one loop the iterator is exhausted
      >>> 
      >>> lst = [x for x in range(3)]
      >>> for n in range(2):
      ...     for i in lst:
      ...         print(i)
      ... 
      0 # 1st print
      1
      2
      0 # 2nd print
      1
      2 
      >>> 
      

      对于等效测试,您应该在每次外循环迭代后重建生成器:

      >>> for n in range(2):
      ...     gen = (i for i in range(3))
      ...     for i in gen:
      ...         print(i)
      ... 
      0 # 1st print
      1
      2
      0 # 2nd print
      1
      2
      >>>
      

      【讨论】:

        猜你喜欢
        • 2013-09-10
        • 1970-01-01
        • 2012-09-27
        • 2020-02-10
        • 1970-01-01
        • 2016-01-20
        • 1970-01-01
        • 2018-05-24
        • 2017-04-06
        相关资源
        最近更新 更多