For 循环与 while 和 next 性能

For loop versus while and next performance

在某些遍历生成器的情况下,使用 whilenext(带有 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 - 如果它必须进入 exceptelsefinally 有更多的开销)但它仍然是开销。这就是为什么你的第二个例子比第三个更快。因此,如果 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。如果你想获得更好的性能:将 tryexcept 放在 while 之外(如果可能)并缓存 next 查找(不要缓存 __next__ lookup,除非你真的知道你会给自己带来什么,你需要挤出更多的性能)。
  • while 方法总是比 for 慢(至少在 CPython 中)并且需要更多的代码。重复一遍:只有在真正需要时才使用它。