Почему код 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 ответа:
вы можете спросить почему хранить локальные переменные быстрее, чем глобальные. Это деталь реализации CPython.
помните, что CPython компилируется в байт-код, который запускает интерпретатор. При компиляции функции локальные переменные хранятся в массиве фиксированного размера (не a
dict
) и имена переменных присваиваются индексы. Это возможно потому, что вы не можете динамически добавлять локальные переменные функции. После получения местных переменная-это буквально поиск указателя в списке и увеличение количества ссылок на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
. Это приводит к сжатию двух кодов операций в один код операции.С другой стороны, элемент
STORE_NAME
код операции используется в цикле на глобальном уровне. Питон делает *не* сделать аналогичные прогнозы, когда он видит этот код операции. Вместо этого он должен вернуться к началу цикла оценки, который имеет очевидные последствия для скорости, с которой выполняется цикл.чтобы дать более подробную техническую информацию об этой оптимизации, вот цитата из
ceval.c
файл ("движок" виртуальной машины Python):некоторые опкоды, как правило, приходят в парах, что позволяет предсказать второй код при запуске первого. Например,
GET_ITER
часто сопровождаетсяFOR_ITER
. ИFOR_ITER
часто далее следуетSTORE_FAST
илиUNPACK_SEQUENCE
.проверка прогноза стоит одного высокоскоростного теста регистра переменный против постоянного. Если спаривание было хорошим, то предикация собственной внутренней ветви процессора имеет высокую вероятность успех, в результате почти нулевыми затратами, переход к следующий код операции. Успешное предсказание сохраняет поездку через eval-цикл включая его две непредсказуемые ветви,