def main():
for i in xrange(10**8):
pass
main()
Этот фрагмент кода на Python выполняется (Примечание: синхронизация выполняется с помощью функции времени в BASH в Linux.)
real 0m1.841s
user 0m1.828s
sys 0m0.012s
Тем не менее, если цикл не помещается в функцию,
for i in xrange(10**8):
pass
тогда он работает намного дольше:
real 0m4.543s
user 0m4.524s
sys 0m0.012s
Почему это?
python
performance
profiling
benchmarking
cpython
thedoctar
источник
источник
Ответы:
Вы можете спросить, почему хранить локальные переменные быстрее, чем глобальные. Это деталь реализации CPython.
Помните, что CPython компилируется в байт-код, который запускает интерпретатор. Когда функция компилируется, локальные переменные сохраняются в массиве фиксированного размера ( не a
dict
), а имена переменных присваиваются индексам. Это возможно, потому что вы не можете динамически добавлять локальные переменные в функцию. Затем извлечение локальной переменной - это буквально поиск указателя в списке и увеличение количества ссылок наPyObject
тривиальное.Сравните это с глобальным поиском (
LOAD_GLOBAL
), который является истиннымdict
поиском, включающим хэш и так далее. Кстати, именно поэтому вам нужно указатьglobal i
, хотите ли вы, чтобы оно было глобальным: если вы когда-либо назначите переменную внутри области видимости, компилятор выдастSTORE_FAST
s для ее доступа, если вы не скажете этого не делать.Кстати, глобальные поиски все еще довольно оптимизированы. Атрибут поиски
foo.bar
являются действительно медленный !Вот небольшая иллюстрация эффективности локальной переменной.
источник
def foo_func: x = 5
,x
локально для функции. Доступx
локальный.foo = SomeClass()
,foo.bar
это атрибут доступа.val = 5
глобальный глобальный. Что касается скорости локального> глобального> атрибута в соответствии с тем, что я прочитал здесь. Таким образом , доступx
вfoo_func
это быстро, а затемval
, после чегоfoo.bar
.foo.attr
это не локальный поиск, потому что в контексте этого условия мы говорим о том, что локальный поиск - это поиск переменной, которая принадлежит функции.globals()
функцию. Если вам нужна дополнительная информация, возможно, вам придется начать поиск исходного кода для Python. А CPython - это просто название для обычной реализации Python - так что вы, вероятно, уже используете его!Внутри функции байт-код:
На верхнем уровне байт-код:
Разница в том, что
STORE_FAST
быстрее (!), ЧемSTORE_NAME
. Это связано с тем, что в функцииi
она локальная, но на верхнем уровне она глобальная.Чтобы проверить байт-код, используйте
dis
модуль . Я был в состоянии разобрать функцию напрямую, но чтобы разобрать код верхнего уровня, мне пришлось использоватьcompile
встроенный .источник
global i
вmain
функцию делает время выполнения эквивалентным.locals()
или, иinspect.getframe()
т. Д.). Поиск элемента массива по постоянному целому числу намного быстрее, чем поиск в dict.Помимо времени хранения локальных / глобальных переменных, предсказание кода операции делает функцию быстрее.
Как объясняют другие ответы, функция использует
STORE_FAST
код операции в цикле. Вот байт-код для цикла функции:Обычно при запуске программы Python выполняет каждый код операции один за другим, отслеживая стек и предварительно формируя другие проверки в кадре стека после выполнения каждого кода операции. Предсказание кода операции означает, что в некоторых случаях Python может перейти непосредственно к следующему коду операции, что позволяет избежать некоторых из этих издержек.
В этом случае каждый раз, когда Python видит
FOR_ITER
(верхнюю часть цикла), он «предсказывает», какойSTORE_FAST
следующий код операции он должен выполнить. Затем Python просматривает следующий код операции и, если прогноз был верным, он сразу переходит кSTORE_FAST
. Это приводит к сжатию двух кодов операций в один код операции.С другой стороны,
STORE_NAME
код операции используется в цикле на глобальном уровне. Python * не * делает подобные прогнозы, когда видит этот код операции. Вместо этого он должен вернуться к началу цикла оценки, что имеет очевидные последствия для скорости выполнения цикла.Чтобы дать некоторые технические подробности об этой оптимизации, вот цитата из
ceval.c
файла («движок» виртуальной машины Python):Мы можем видеть в исходном коде для
FOR_ITER
кода операции именно то, гдеSTORE_FAST
сделан прогноз для :PREDICT
Функция расширяется,if (*next_instr == op) goto PRED_##op
то есть мы просто перейти к началу прогнозируемого опкода. В этом случае мы прыгаем здесь:Теперь установлена локальная переменная, и следующий код операции готов к выполнению. Python продолжает итерацию до конца, каждый раз делая успешный прогноз.
Вики - странице Python имеет больше информации о том , как работает виртуальная машина CPython в.
источник
HAS_ARG
тест никогда не выполняется (за исключением случаев, когда трассировка низкого уровня включена как во время компиляции, так и во время выполнения, что не выполняется в обычной сборке), оставляя только один непредсказуемый переход.PREDICT
макрос полностью отключается; вместо этого большинство случаев заканчиваются наDISPATCH
том, что ответвляется напрямую. Но на процессорах с предсказанием ветвлений эффект аналогичен таковомуPREDICT
, поскольку ветвление (и предсказание) выполняется для каждого кода операции, что увеличивает шансы успешного предсказания ветвления.