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

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

этот фрагмент кода в Python запускается (Примечание: синхронизация выполняется с помощью функции time в 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 ответов


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

помните, что CPython компилируется в байт-код, который запускается интерпретатором. При компиляции функции локальные переменные хранятся в массиве фиксированного размера (не a dict) и имена переменных присваиваются индексы. Это возможно потому, что вы не можете динамически добавлять локальные переменные функции. Затем получение локального переменная-это буквально поиск указателя в списке и увеличение refcount на 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. Это приводит к сжатию двух опкодов в один опкод.

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

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

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

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