Почему код Python работает быстрее в функции?


def main():
    for i in xrange(10**8):
        pass
main()

этот фрагмент кода в Python работает (Примечание: синхронизация выполняется с помощью функции времени в BASH в Linux.)

real    0m1.841s
user    0m1.828s
sys     0m0.012s

однако, если цикл for не помещается в функцию,

for i in xrange(10**8):
    pass

затем он работает в течение гораздо более длительного времени:

real    0m4.543s
user    0m4.524s
sys     0m0.012s

Почему это?

3 745

3 ответа:

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

помните, что CPython компилируется в байт-код, который запускает интерпретатор. При компиляции функции локальные переменные хранятся в массиве фиксированного размера (не a dict) и имена переменных присваиваются индексы. Это возможно потому, что вы не можете динамически добавлять локальные переменные функции. После получения местных переменная-это буквально поиск указателя в списке и увеличение количества ссылок на PyObject что тривиально.

сравните это с глобальным поиском (LOAD_GLOBAL), который является истинным dict поиск по хэш и так далее. Кстати, именно поэтому вам нужно указать global i если вы хотите, чтобы он был глобальным: если вы когда-либо назначаете переменную внутри области, компилятор выдаст STORE_FASTs для его доступа, если вы не скажете ему не делать.

кстати, глобальные поиски все еще довольно оптимизировано. Поиск атрибутов foo.bar это действительно медленные!

вот маленький иллюстрации о локальной переменной эффективности.

внутри функции байт-код

  2           0 SETUP_LOOP              20 (to 23)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_FAST               0 (i)

  3          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               0 (None)
             26 RETURN_VALUE        

на верхнем уровне байт-код

  1           0 SETUP_LOOP              20 (to 23)
              3 LOAD_NAME                0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_NAME               1 (i)

  2          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               2 (None)
             26 RETURN_VALUE        

разница в том, что STORE_FAST быстрее (!), чем STORE_NAME. Это потому, что в функции i является локальным, но на верхнем уровне это глобальный.

чтобы проверить байт-код, используйте dis модуль. Я смог разобрать функцию напрямую, но для разборки кода верхнего уровня мне пришлось использовать compile builtin.

помимо локальных / глобальных переменных времени хранения,прогнозирование в коде делает функцию быстрее.

как объясняют другие ответы, функция использует STORE_FAST операции в цикле. Вот байт-код для цикла функции:

    >>   13 FOR_ITER                 6 (to 22)   # get next value from iterator
         16 STORE_FAST               0 (x)       # set local variable
         19 JUMP_ABSOLUTE           13           # back to FOR_ITER

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

в этом случае, каждый раз, когда Python видит FOR_ITER (в верхней части цикла), он будет "предсказать", что STORE_FAST - Это следующий код операции, который он должен выполнить. Затем Python заглядывает в следующий код операции и, если прогноз был правильным, он переходит прямо к STORE_FAST. Это приводит к сжатию двух кодов операций в один код операции.

С другой стороны, элемент STORE_NAME код операции используется в цикле на глобальном уровне. Питон делает *не* сделать аналогичные прогнозы, когда он видит этот код операции. Вместо этого он должен вернуться к началу цикла оценки, который имеет очевидные последствия для скорости, с которой выполняется цикл.

чтобы дать более подробную техническую информацию об этой оптимизации, вот цитата из ceval.c файл ("движок" виртуальной машины Python):

некоторые опкоды, как правило, приходят в парах, что позволяет предсказать второй код при запуске первого. Например, GET_ITER часто сопровождается FOR_ITER. И FOR_ITER часто далее следует STORE_FAST или UNPACK_SEQUENCE.

проверка прогноза стоит одного высокоскоростного теста регистра переменный против постоянного. Если спаривание было хорошим, то предикация собственной внутренней ветви процессора имеет высокую вероятность успех, в результате почти нулевыми затратами, переход к следующий код операции. Успешное предсказание сохраняет поездку через eval-цикл включая его две непредсказуемые ветви,