For 循环与 while 和 next 性能
For loop versus while and next performance
在某些遍历生成器的情况下,使用 while
和 next
(带有 try/except StopIteration
)似乎比更简单的 for 循环更自然。然而,这带来了显着的性能成本。
这里发生了什么,做出选择的正确方法是什么?
请参阅下面的示例代码和时序:
%%timeit
for x in gen():
pass
# 180 µs ± 8.78 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%%timeit
_gen = gen()
try:
while True:
x = next(_gen)
except StopIteration:
pass
# 606 µs ± 19.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
# Alternative use of next: But I don't see any good reason to use it.
%%timeit
_gen = gen()
while True:
try:
x = next(_gen)
except StopIteration:
break
# 676 µs ± 24.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
大多数时候你应该使用 for
循环。它为您做了一些事情,您自己做可能会很乏味:
- 适用于可迭代对象和迭代器。
- 为您处理
StopIteration
(在 CPython 中,StopIteration
是在 C 中处理的,而不是 Python,因此速度明显更快)
这意味着您可以使用 for
获得更通用的代码和更快的代码。所以它应该永远是首选。
然而,在某些情况下您不能使用 for
循环,那么 while
循环是一个不错的选择。为了使其更通用,您还应该在参数上使用 iter
,这样您还可以处理不是迭代器的可迭代对象:
_gen = iter(gen())
...
您需要问自己的下一个问题是:您需要为每个 next
调用处理 StopIteration
还是与 StopIteration
发生的位置无关?
Entering/Leaving try
没有太多开销(仅适用于 try
- 如果它必须进入 except
,else
或 finally
有更多的开销)但它仍然是开销。这就是为什么你的第二个例子比第三个更快。因此,如果 StopIteration
来自哪里并不重要,那么将 while True
包装在 try
中将是更快的选择:
try:
while True:
next(_gen)
except StopIteration:
pass
有几个选项可以使 while
方法更快。一种方法是避免每次迭代发生一次 next
的全局名称查找。
通过使用局部变量,此查找成本仅发生一次,并且循环内的本地名称查找速度更快:
def f(gen):
_gen = iter(gen())
_next = next
try:
while True:
x = _next(_gen)
except StopIteration:
return
如果我必须使用 while
循环方法,那将是我最喜欢的方法。
您甚至可以更进一步,避免每次调用 next
时发生的 __next__
查找。然而,这会(在某些情况下)偏离纯粹的 next
行为,并且只有 如果您知道自己在做什么 并且只有当您 真的需要非常小的性能提升 这给你。一般来说,你 不应该使用 :
def f(gen):
_gen = iter(gen())
_next = _gen.__next__
try:
while True:
x = _next()
except StopIteration:
return
但是我不推荐这种方法。而且不应该直接调用双下划线函数。我只是为了完整性才提到它。
我还做了一个基准测试来显示这些方法的性能:
from simple_benchmark import BenchmarkBuilder
b = BenchmarkBuilder()
@b.add_function()
def for_loop(gen):
for i in gen:
pass
@b.add_function()
def while_outer_try(gen):
_gen = iter(gen)
try:
while True:
x = next(_gen)
except StopIteration:
pass
@b.add_function()
def while_inner_try(gen):
_gen = iter(gen)
while True:
try:
x = next(_gen)
except StopIteration:
break
@b.add_function()
def while_outer_try_cache_next(gen):
_gen = iter(gen)
_next = next
try:
while True:
x = _next(_gen)
except StopIteration:
return
@b.add_function()
def while_outer_try_cache_next_method(gen):
_gen = iter(gen)
_next = _gen.__next__
try:
while True:
x = _next()
except StopIteration:
return
@b.add_arguments('length')
def argument_provider():
for exp in range(2, 20):
size = 2**exp
yield size, range(size)
r = b.run()
r.plot()
总结:
- 尽可能使用
for
循环方法。
- 当您使用
while
方法时,请确保您在可迭代对象上使用 iter
。如果你想获得更好的性能:将 try
和 except
放在 while
之外(如果可能)并缓存 next
查找(不要缓存 __next__
lookup,除非你真的知道你会给自己带来什么,你需要挤出更多的性能)。
while
方法总是比 for
慢(至少在 CPython 中)并且需要更多的代码。重复一遍:只有在真正需要时才使用它。
在某些遍历生成器的情况下,使用 while
和 next
(带有 try/except StopIteration
)似乎比更简单的 for 循环更自然。然而,这带来了显着的性能成本。
这里发生了什么,做出选择的正确方法是什么?
请参阅下面的示例代码和时序:
%%timeit
for x in gen():
pass
# 180 µs ± 8.78 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%%timeit
_gen = gen()
try:
while True:
x = next(_gen)
except StopIteration:
pass
# 606 µs ± 19.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
# Alternative use of next: But I don't see any good reason to use it.
%%timeit
_gen = gen()
while True:
try:
x = next(_gen)
except StopIteration:
break
# 676 µs ± 24.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
大多数时候你应该使用 for
循环。它为您做了一些事情,您自己做可能会很乏味:
- 适用于可迭代对象和迭代器。
- 为您处理
StopIteration
(在 CPython 中,StopIteration
是在 C 中处理的,而不是 Python,因此速度明显更快)
这意味着您可以使用 for
获得更通用的代码和更快的代码。所以它应该永远是首选。
然而,在某些情况下您不能使用 for
循环,那么 while
循环是一个不错的选择。为了使其更通用,您还应该在参数上使用 iter
,这样您还可以处理不是迭代器的可迭代对象:
_gen = iter(gen())
...
您需要问自己的下一个问题是:您需要为每个 next
调用处理 StopIteration
还是与 StopIteration
发生的位置无关?
Entering/Leaving try
没有太多开销(仅适用于 try
- 如果它必须进入 except
,else
或 finally
有更多的开销)但它仍然是开销。这就是为什么你的第二个例子比第三个更快。因此,如果 StopIteration
来自哪里并不重要,那么将 while True
包装在 try
中将是更快的选择:
try:
while True:
next(_gen)
except StopIteration:
pass
有几个选项可以使 while
方法更快。一种方法是避免每次迭代发生一次 next
的全局名称查找。
通过使用局部变量,此查找成本仅发生一次,并且循环内的本地名称查找速度更快:
def f(gen):
_gen = iter(gen())
_next = next
try:
while True:
x = _next(_gen)
except StopIteration:
return
如果我必须使用 while
循环方法,那将是我最喜欢的方法。
您甚至可以更进一步,避免每次调用 next
时发生的 __next__
查找。然而,这会(在某些情况下)偏离纯粹的 next
行为,并且只有 如果您知道自己在做什么 并且只有当您 真的需要非常小的性能提升 这给你。一般来说,你 不应该使用 :
def f(gen):
_gen = iter(gen())
_next = _gen.__next__
try:
while True:
x = _next()
except StopIteration:
return
但是我不推荐这种方法。而且不应该直接调用双下划线函数。我只是为了完整性才提到它。
我还做了一个基准测试来显示这些方法的性能:
from simple_benchmark import BenchmarkBuilder
b = BenchmarkBuilder()
@b.add_function()
def for_loop(gen):
for i in gen:
pass
@b.add_function()
def while_outer_try(gen):
_gen = iter(gen)
try:
while True:
x = next(_gen)
except StopIteration:
pass
@b.add_function()
def while_inner_try(gen):
_gen = iter(gen)
while True:
try:
x = next(_gen)
except StopIteration:
break
@b.add_function()
def while_outer_try_cache_next(gen):
_gen = iter(gen)
_next = next
try:
while True:
x = _next(_gen)
except StopIteration:
return
@b.add_function()
def while_outer_try_cache_next_method(gen):
_gen = iter(gen)
_next = _gen.__next__
try:
while True:
x = _next()
except StopIteration:
return
@b.add_arguments('length')
def argument_provider():
for exp in range(2, 20):
size = 2**exp
yield size, range(size)
r = b.run()
r.plot()
总结:
- 尽可能使用
for
循环方法。 - 当您使用
while
方法时,请确保您在可迭代对象上使用iter
。如果你想获得更好的性能:将try
和except
放在while
之外(如果可能)并缓存next
查找(不要缓存__next__
lookup,除非你真的知道你会给自己带来什么,你需要挤出更多的性能)。 while
方法总是比for
慢(至少在 CPython 中)并且需要更多的代码。重复一遍:只有在真正需要时才使用它。