Почему цикл через range () в Python выполняется быстрее, чем при использовании цикла while?

На днях я проводил тестирование Python и наткнулся на кое-что интересное. Ниже приведены две петли, которые делают примерно то же самое. Цикл 1 занимает примерно вдвое больше времени, чем цикл 2.

Петля 1:

int i = 0
while i < 100000000:
  i += 1

Цикл 2:

for n in range(0,100000000):
  pass

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


person A. Dorton    schedule 15.05.2009    source источник


Ответы (6)


см. разборку байтового кода python, вы можете получить более конкретное представление

использовать цикл while:

1           0 LOAD_CONST               0 (0)
            3 STORE_NAME               0 (i)

2           6 SETUP_LOOP              28 (to 37)
      >>    9 LOAD_NAME                0 (i)              # <-
           12 LOAD_CONST               1 (100000000)      # <-
           15 COMPARE_OP               0 (<)              # <-
           18 JUMP_IF_FALSE           14 (to 35)          # <-
           21 POP_TOP                                     # <-

3          22 LOAD_NAME                0 (i)              # <-
           25 LOAD_CONST               2 (1)              # <-
           28 INPLACE_ADD                                 # <-
           29 STORE_NAME               0 (i)              # <-
           32 JUMP_ABSOLUTE            9                  # <-
      >>   35 POP_TOP
           36 POP_BLOCK

В теле петли 10 оп.

диапазон использования:

1           0 SETUP_LOOP              23 (to 26)
            3 LOAD_NAME                0 (range)
            6 LOAD_CONST               0 (0)
            9 LOAD_CONST               1 (100000000)
           12 CALL_FUNCTION            2
           15 GET_ITER
      >>   16 FOR_ITER                 6 (to 25)        # <-
           19 STORE_NAME               1 (n)            # <-

2          22 JUMP_ABSOLUTE           16                # <-
      >>   25 POP_BLOCK
      >>   26 LOAD_CONST               2 (None)
           29 RETURN_VALUE

В теле цикла имеется 3 оп.

Время выполнения кода C намного короче, чем у интерпретатора, и его можно игнорировать.

person kcwu    schedule 15.05.2009
comment
Собственно тело петли при первой разборке имеет 10 операций (переход с позиции 32 на 9). В текущей реализации CPython интерпретация каждого байт-кода с довольно высокой вероятностью приводит к дорогостоящему непрямому неверному предсказанию перехода в ЦП (переход к реализации следующего байт-кода). Это является следствием текущей реализации CPython, JIT, реализуемые незагруженным Swallow, PyPy и другими, скорее всего, потеряют эти накладные расходы. Лучшие из них также смогут делать специализацию шрифтов для ускорения на порядок. - person Ants Aasma; 16.05.2009
comment
используйте модуль dis. Определите свой код в функции, затем вызовите dis.disco (func .__ code__) - person kcwu; 28.06.2013
comment
Можно ли тогда сказать, что на более высоком уровне цикл while должен выполнять сравнение на каждой итерации? - person davidhood2; 20.10.2016
comment
Но ваш дизассемблированный код исключает range (). Я полагаю, что ответ Георга Шелли правильный, но в нем отсутствует доказательство. - person Michael; 14.06.2021
comment
re @Michael, в принципе мы оба правы, просто опиши по-разному - person kcwu; 15.06.2021

range() реализован в C, тогда как i += 1 интерпретируется.

Использование xrange() может сделать его еще быстрее для больших чисел. Начиная с Python 3.0 range() совпадает с предыдущим xrange().

person Georg Schölly    schedule 15.05.2009

Надо сказать, что в цикле while происходит много создания и уничтожения объектов.

i += 1

такой же как:

i = i + 1

Но поскольку целые значения Python неизменяемы, он не изменяет существующий объект; скорее он создает совершенно новый объект с новой ценностью. Это в основном:

i = new int(i + 1)   # Using C++ or Java-ish syntax

Сборщику мусора также предстоит большая работа по очистке. «Создание объекта - дорогое удовольствие».

person Peter    schedule 18.04.2013

Потому что вы чаще работаете с кодом, написанным на C в интерпретаторе. то есть i + = 1 находится в Python, поэтому он медленный (сравнительно), тогда как range (0, ...) - это один вызов C, цикл for будет выполняться в основном и в C.

person John Montgomery    schedule 15.05.2009

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

В частности, в случае цикла for в C происходят две вещи, которые в цикле while обрабатываются в Python:

  1. В цикле while сравнение i < 100000000 выполняется в Python, тогда как в цикле for задание передается итератору range(100000000), который внутренне выполняет итерацию (и, следовательно, проверку границ) в C.

  2. В цикле while обновление цикла i += 1 происходит в Python, тогда как в цикле for итератор range(100000000), написанный на C, выполняет i+=1 (или ++i).

Мы видим, что комбинация этих двух вещей ускоряет цикл for, добавляя их вручную, чтобы увидеть разницу.

import timeit

N = 100000000


def while_loop():
    i = 0
    while i < N:
        i += 1


def for_loop_pure():
    for i in range(N):
        pass


def for_loop_with_increment():
    for i in range(N):
        i += 1


def for_loop_with_test():
    for i in range(N):
        if i < N: pass


def for_loop_with_increment_and_test():
    for i in range(N):
        if i < N: pass
        i += 1


def main():
    print('while loop\t\t', timeit.timeit(while_loop, number=1))
    print('for pure\t\t', timeit.timeit(for_loop_pure, number=1))
    print('for inc\t\t\t', timeit.timeit(for_loop_with_increment, number=1))
    print('for test\t\t', timeit.timeit(for_loop_with_test, number=1))
    print('for inc+test\t', timeit.timeit(for_loop_with_increment_and_test, number=1))


if __name__ == '__main__':
    main()

Я пробовал это и с числом 100000000, буквальной константой, и с переменной N, что было бы более типично.

# inline constant N
while loop      3.5131139
for pure        1.3211338000000001
for inc         3.5477727000000003
for test        2.5209639
for inc+test    4.697028999999999

# variable N
while loop      4.1298240999999996
for pure        1.3526357999999998
for inc         3.6060175
for test        3.1093069
for inc+test    5.4753364

Как видите, в обоих случаях время while очень близко к разнице for inc+test и for pure. Также обратите внимание, что в случае, когда мы используем переменную N, while имеет дополнительное замедление для повторного поиска значения N, но for этого не делает.

Это действительно безумие, что такие тривиальные модификации могут привести к более чем 3-кратному ускорению кода, но это Python для вас. И даже не заставляйте меня начинать, когда вы вообще можете использовать встроенную функцию над циклом ....

person mCoding    schedule 16.12.2020

Большинство вызовов встроенных методов Python выполняются как код C. Код, который нужно интерпретировать, намного медленнее. С точки зрения эффективности памяти и скорости выполнения разница колоссальная. Внутреннее устройство Python было оптимизировано до крайности, и лучше всего воспользоваться этой оптимизацией.

person ohdeargod    schedule 15.05.2009