Для цикла против времени и следующего исполнения - PullRequest
3 голосов
/ 24 сентября 2019

В некоторых случаях зацикливания генератора кажется более естественным использовать while и nexttry/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)

1 Ответ

2 голосов
/ 28 сентября 2019

Большую часть времени вы всегда должны использовать цикл for.Это несколько вещей для вас, что вы, вероятно, не хотите обрабатывать себя:

  • Работает для итераторов и итераторов
  • Обрабатывает StopIteration для вас (в CPython StopIteration это дескрипторы в C вместо Python, что делает его значительно быстрее)

Это означает, что у вас есть более общий код и более быстрый код с for.Так что это всегда должен быть предпочтительный вариант.

Однако в некоторых случаях вы на самом деле не можете использовать for, тогда цикл while действительно хороший выбор.Чтобы сделать его более общим, вы также должны использовать iter в качестве аргумента, чтобы вы также могли обрабатывать итерации, которые не являются итераторами:

_gen = iter(gen())
...

Следующий вопрос, который вам нужно задать себе: Вам нужнообрабатывать StopIteration для каждого next вызова или не имеет значения, где происходит StopIteration?Ввод 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 и должно выполняться только , если вы знаете, что делаете , и только если вам действительно нужна очень маленькая производительностьBoost это дает вам.В общем, вы НЕ должны использовать это :

def f(gen):
    _gen = iter(gen())
    _next = _gen.__next__
    try:
        while True:
            x = _next()
    except StopIteration:
        return

Однако я не рекомендую этот подход, вы найдете некоторые действительно странные и странные проблемы с этим подходом (он может привести к разным результатам).И в действительности нельзя вызывать функции двойного подчеркивания напрямую.Я просто упомянул это для полноты.


Я также сделал тест для отображения производительности этих подходов:

enter image description here

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__, кроме тех случаев, когда вы действительно знаетечто вы принесете на себя, и вам нужно выжать еще больше производительности).
  • Подход while всегда будет медленнее, чем for (по крайней мере, в CPython) и потребует значительно больше кода.Повторюсь: используйте его только если это действительно необходимо.
...