`yield from` 生成器 vs `yield from` 列表性能
`yield from` generator vs `yield from` list performance
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
列出了很多次。列表版本总是提供更好的性能,而我的直觉告诉我相反的结论——制作列表需要在启动时分配内存。为什么我们可以注意到这样的性能差异?
简短的回答是表面语法使它们看起来比实际更相似
我将更详细地分解一系列功能(dis
模块对此很有帮助),我会将其分为设置成本和每个产生值的成本。我们开始于:
def yield_from_generator():
yield from (i for i in range(10000))
成本是:
- 设置:创建范围对象并调用嵌入式生成器表达式
- per-yield:来自
genexpr
的 yield,它还会在 range
迭代器上调用 next
。注意这里有两个上下文切换
接下来我们看:
def yield_from_list():
yield from [i for i in range(10000)]
成本是:
- 设置:创建一个新列表并使用列表理解来填充它。这使用特殊的
list
操作码所以会很快
- per-yield:只是恢复
list
的迭代器所以速度很快
接下来我们看一个类似的函数:
def yield_from_list2():
yield from list(i for i in range(10000))
这不使用特殊列表操作码并且具有生成器的双重嵌套,因此再次变慢。成本是:
- 设置:创建一个新的生成器表达式并将其传递给列表构造函数,这将迭代遍历范围对象的生成器表达式
- per-yield:使用
list
的迭代器,所以又快了
最后一个快速版本只是强调 yield from
:
def yield_from_generator2():
yield from range(10000)
成本是:
- 设置:创建一个
range
对象
- per-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 微秒,但希望更明显的是为什么它位于中间,性能明智
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
列出了很多次。列表版本总是提供更好的性能,而我的直觉告诉我相反的结论——制作列表需要在启动时分配内存。为什么我们可以注意到这样的性能差异?
简短的回答是表面语法使它们看起来比实际更相似
我将更详细地分解一系列功能(dis
模块对此很有帮助),我会将其分为设置成本和每个产生值的成本。我们开始于:
def yield_from_generator():
yield from (i for i in range(10000))
成本是:
- 设置:创建范围对象并调用嵌入式生成器表达式
- per-yield:来自
genexpr
的 yield,它还会在range
迭代器上调用next
。注意这里有两个上下文切换
接下来我们看:
def yield_from_list():
yield from [i for i in range(10000)]
成本是:
- 设置:创建一个新列表并使用列表理解来填充它。这使用特殊的
list
操作码所以会很快 - per-yield:只是恢复
list
的迭代器所以速度很快
接下来我们看一个类似的函数:
def yield_from_list2():
yield from list(i for i in range(10000))
这不使用特殊列表操作码并且具有生成器的双重嵌套,因此再次变慢。成本是:
- 设置:创建一个新的生成器表达式并将其传递给列表构造函数,这将迭代遍历范围对象的生成器表达式
- per-yield:使用
list
的迭代器,所以又快了
最后一个快速版本只是强调 yield from
:
def yield_from_generator2():
yield from range(10000)
成本是:
- 设置:创建一个
range
对象 - per-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 微秒,但希望更明显的是为什么它位于中间,性能明智