Почему код 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
Почему это?
Ответы
Ответ 1
Вы можете спросить, почему быстрее хранить локальные переменные, чем глобальные. Это деталь реализации CPython.
Помните, что CPython скомпилирован в байт-код, который выполняет интерпретатор. Когда функция компилируется, локальные переменные хранятся в массиве фиксированного размера (а не dict
), а имена переменных присваиваются индексам. Это возможно, потому что вы не можете динамически добавлять локальные переменные в функцию. Затем получение локальной переменной - это буквальный поиск указателя в списке и увеличение количества ссылок на PyObject
, которое тривиально.
Сравните это с глобальным поиском (LOAD_GLOBAL
), который является истинным поиском dict
, включающим хеш и т.д. Кстати, вот почему вам нужно указать global i
, если вы хотите, чтобы он был глобальным: если вы когда-либо назначали переменную внутри области, компилятор выдаст STORE_FAST
для своего доступа, если вы не сообщите об этом не.
Кстати, глобальный поиск по-прежнему довольно оптимизирован. Поиск атрибутов foo.bar
- очень медленные!
Ниже представлена небольшая иллюстрация по эффективности локальной переменной.
Ответ 2
Внутри функции байт-код
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
встроенный.
Ответ 3
Помимо локального/глобального времени хранения переменных, опкод-предсказание делает функцию быстрее.
Как объясняют другие ответы, функция использует код операции 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 выполняет каждый код операции один за другим, отслеживая стеки и предварительно формируя другие проверки в кадре стека после выполнения каждого кода операции. Прогнозирование кода означает, что в некоторых случаях Python может перейти непосредственно к следующему коду операции, тем самым избегая некоторых из этих накладных расходов.
В этом случае каждый раз, когда Python видит FOR_ITER
(верх цикла), он "предсказывает", что STORE_FAST
является следующим STORE_FAST
операции, который он должен выполнить. Затем Python заглядывает в следующий код операции и, если предсказание верное, оно переходит прямо к STORE_FAST
. Это приводит к сжатию двух кодов операций в один код операции.
С другой стороны, код операции STORE_NAME
используется в цикле на глобальном уровне. Python делает * not * делает подобные прогнозы, когда видит этот код операции. Вместо этого он должен вернуться к вершине цикла оценки, который имеет очевидные последствия для скорости, с которой выполняется цикл.
Чтобы дать дополнительную техническую информацию об этой оптимизации, здесь приводится цитата из файла ceval.c
("движок" виртуальной машины Python):
Некоторые коды операций, как правило, попадают в пары, что позволяет прогнозировать второй код при первом запуске. Например, GET_ITER
часто сопровождается FOR_ITER
. За FOR_ITER
часто следуют STORE_FAST
или UNPACK_SEQUENCE
.
Проверка прогноза требует одного высокоскоростного теста переменной регистра с константой. Если спаривание было хорошим, то собственное внутреннее прерывание ветвления процессора имеет высокую вероятность успеха, что приводит к почти нулевому переходу на следующий код операции. Успешное предсказание экономит поездку через eval-loop, включая две непредсказуемые ветки, тест HAS_ARG
и коммутационный футляр. В сочетании с предсказанием внутренней ветки процессора успешный PREDICT
приводит к тому, что два PREDICT
работают так, как если бы они были одним новым кодом операции с объединенными телами.
Мы можем видеть в исходном коде код операции FOR_ITER
точно, где сделано предсказание для STORE_FAST
:
case FOR_ITER: // the FOR_ITER opcode case
v = TOP();
x = (*v->ob_type->tp_iternext)(v); // x is the next value from iterator
if (x != NULL) {
PUSH(x); // put x on top of the stack
PREDICT(STORE_FAST); // predict STORE_FAST will follow - success!
PREDICT(UNPACK_SEQUENCE); // this and everything below is skipped
continue;
}
// error-checking and more code for when the iterator ends normally
Функция PREDICT
расширяется до if (*next_instr == op) goto PRED_##op
т.е. мы просто переходим к началу прогнозируемого кода операции. В этом случае мы прыгаем сюда:
PREDICTED_WITH_ARG(STORE_FAST);
case STORE_FAST:
v = POP(); // pop x back off the stack
SETLOCAL(oparg, v); // set it as the new local variable
goto fast_next_opcode;
Локальная переменная теперь установлена, и следующий код операции готов к выполнению. Python продолжается через iterable, пока он не достигнет конца, делая успешное предсказание каждый раз.
На вики-странице Python есть больше информации о том, как работает виртуальная машина CPython.
Ответ 4
Если в Python существует name == " main":..., чтобы наши файлы Python могли выступать в качестве повторно используемых модулей или как автономные программы.
Давайте возьмем пример с двумя модулями python (.py): -
# mymath.py
def square(x):
return x * x
if __name__ == '__main__':
print "test: square(6) ==", square(6)
#mygame.py
import mymath
print "this is mygame."
print mymath.square(11)
В этом примере weve написал mymath.py для использования в качестве служебного модуля, а также для отдельной программы. Мы можем запускать mymath отдельно, делая это:
python mymath.py
test: square(6) == 36
Но мы также можем использовать mymath.py в качестве модуля; Давайте посмотрим, что произойдет, когда запустите mygame.py:
python mygame.py
this is mygame.
121
Обратите внимание, что здесь мы не видим "тестовую линию, с которой mymath.py был близок нижней части его кода. То потому что, в этом контексте, mymath не основная программа. Thats what if name == " main":... трюк используется для.