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

81

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

Петля 1:

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

Цикл 2:

for n in range(0,100000000):
  pass

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

А. Дортон
источник

Ответы:

159

см. дизассемблер байтового кода 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 намного короче, чем у интерпретатора, и его можно игнорировать.

KCWU
источник
2
Собственно тело петли при первой разборке имеет 10 операций (переход с позиции 32 на 9). В текущей реализации CPython интерпретация каждого байт-кода с довольно высокой вероятностью приводит к дорогостоящему непрямому неверному предсказанию перехода в ЦП (переход к реализации следующего байт-кода). Это является следствием текущей реализации CPython, JIT, реализуемые незагруженным Swallow, PyPy и другими, скорее всего, потеряют эти накладные расходы. Лучшие из них также смогут делать специализацию шрифтов для ускорения на порядок.
Ants Aasma
5
используйте модуль "dis". Определите свой код в функции, затем вызовите dis.disco (func .__ code__)
kcwu
Можно ли тогда сказать, что на более высоком уровне whileцикл должен выполнять сравнение на каждой итерации?
davidhood2
35

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

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

Георг Шёлли
источник
15

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

i += 1

такой же как:

i = i + 1

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

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

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

Питер
источник
4

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

Джон Монтгомери
источник
1

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

о Боже мой
источник
0

Я думаю, что ответ здесь немного более тонкий, чем предполагают другие ответы, хотя суть его верна: цикл 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 для вас. И даже не заставляйте меня начинать, когда вы вообще можете использовать встроенную функцию над циклом ....

mCoding
источник