`yield from`生成器与`yield from`列表的性能比较

6
Python 3.6.8 (default, Oct  7 2019, 12:59:55) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.9.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: def yield_from_generator(): 
   ...:     yield from (i for i in range(10000)) 
   ...:                                                                                                                                    

In [2]: def yield_from_list(): 
   ...:     yield from [i for i in range(10000)] 
   ...:                                                                                                                                    

In [3]: import timeit                                                                                                                      

In [4]: timeit.timeit(lambda: list(yield_from_generator()), number=10000)                                                                  
Out[4]: 5.3820097140014695

In [5]: timeit.timeit(lambda: list(yield_from_list()), number=10000)                                                                       
Out[5]: 4.333915593000711

我多次运行了yield from生成器和yield from列表。总是发现列表版本具有更好的性能,而我的直觉告诉我相反的结论 - 制作列表需要在启动时进行内存分配。为什么我们会注意到这种性能差异?


4
列表占用更多内存并不意味着它应该具有更好的时间性能。调用next()(并潜在地抛出StopIteration)可能会很昂贵。 - DeepSpace
2
你看不到分配10000个项目列表的成本,因为这只会在第一次调用yield from list时发生,并且在随后的9999个从列表中产生的yield中被稀释(可能非常快)。 - DisappointedByUnaccountableMod
1
请注意,在Python 3.8中,“yield_from_generator”速度(略微)更快。 - Sam Mason
1
我正在使用Python-3.8.1版本,但生成器仍然很慢。yield from与普通的return基本上给出了相同的结果,因此它并不相关。这个问题似乎是这个问题的重复:https://dev59.com/WWct5IYBdhLWcg3wmOng。 - ekhumoro
4
那个旧的骗子目标正在讨论Python 2的内容;除了yield from不在Python 2中存在以外,Python 3中的列表推导式略有不同:它们现在会创建自己的作用域,但是如果我没记错的话,一些改进已经被实现,使得它们拥有自己的作用域对速度没有太大影响。因此,我不确定是否应该将这个问题关闭为Python 2的重复。 - PM 2Ring
显示剩余20条评论
1个回答

3

简单来说,表面语法使它们看起来比它们实际上更相似。

我将更详细地介绍一系列功能(dis模块对此非常有帮助),我将把事情分成设置成本和每个生成值的成本。我们从以下内容开始:

def yield_from_generator():
    yield from (i for i in range(10000))

费用如下:

  • 设置: 创建范围对象并调用嵌入式生成器表达式
  • 每次产出: 从genexpr中产出,这也会在range迭代器上调用next。请注意这里有两个上下文切换

接下来我们看:

def yield_from_list():
    yield from [i for i in range(10000)]

费用包括:

  • 设置:创建一个新的列表并使用列表推导式填充它。这将使用特殊的list操作码,因此速度快。
  • 每次yield: 只需恢复list的迭代器,因此速度很快。

接下来我们看一个类似的函数:

def yield_from_list2():
    yield from list(i for i in range(10000))

这个方法不使用特殊的列表操作码,并且具有生成器的双重嵌套,因此速度较慢。成本如下:

  • 设置:创建一个新的生成器表达式并将其传递给列表构造函数,这将迭代在范围对象上迭代的生成器表达式
  • 每个yield:使用list的迭代器,因此速度再次快

最后是一个使用yield from的快速版本:

def yield_from_generator2():
    yield from range(10000)

费用如下:

  • 设置:创建一个range对象
  • 每个yield:直接恢复range迭代器

在我的笔记本电脑上,所有这些操作的时间如下:

yield_from_generator  639 µs
yield_from_list       536 µs
yield_from_list2      689 µs
yield_from_generator2 354 µs

希望现在更清楚了。另一个版本是:
def yield_from_list3():
    yield from list(range(10000))

这个程序运行时间只需要401微秒,但是这个位置在性能方面更显而易见。


@HeapOverflow 我之前使用“生成器表达式”这个术语来指代隐藏的嵌入式函数。但这并不正确(我已经更改了术语),但我不确定如何解释它或者有什么好的参考资料。 - Sam Mason

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接