Почему код 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":... трюк используется для.