Почему код 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_FAST
s для его доступа, если вы не скажете ему не делать этого.
кстати, глобальные поиски по-прежнему довольно оптимистично. Поиск атрибутов 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-циклу включая две его непредсказуемые ветви,