Глобальный код в два раза медленнее локального

Почему код на уровне модуля исполняется в два раза медленнее такого же кода в функции?

global.py local.py

s = 0
for i in range(100_000_000):
s += i
print(s)



def main():
s = 0
for i in range(100_000_000):
s += i
print(s)


main()
Десять секунд: Против пяти:
$ time -p python global.py
4999999950000000
real 9.58
user 9.56
sys 0.00
$ time -p python local.py
4999999950000000
real 4.76
user 4.75
sys 0.00

Проверено на CPython 3.7, 3.10, 3.12.


Ответы (1 шт):

Автор решения: Stanislav Volodarskiy

Сравним байт коды. Я оставил только то что относится к циклу. Тела циклов начинаются инструкцией FOR_ITER и завершаются JUMP_BACKWARD. В телах выделены отличающиеся инструкции:

global.py local.py

s = 0
for i in range(100_000_000):
s += i
print(s)



def main():
s = 0
for i in range(100_000_000):
s += i
print(s)


main()
...
6 PUSH_NULL
8 LOAD_NAME 1 (range)
10 LOAD_CONST 1 (100000000)
12 PRECALL 1
16 CALL 1
26 GET_ITER
>> 28 FOR_ITER 7 (to 44)
30 STORE_NAME 2 (i)

32 LOAD_NAME 0 (s)
34 LOAD_NAME 2 (i)
36 BINARY_OP 13 (+=)
40 STORE_NAME 0 (s)
42 JUMP_BACKWARD 8 (to 28)
...
...

6 LOAD_GLOBAL 1 (NULL + range)
18 LOAD_CONST 2 (100000000)
20 PRECALL 1
24 CALL 1
34 GET_ITER
>> 36 FOR_ITER 7 (to 52)
38 STORE_FAST 1 (i)

40 LOAD_FAST 0 (s)
42 LOAD_FAST 1 (i)
44 BINARY_OP 13 (+=)
48 STORE_FAST 0 (s)
50 JUMP_BACKWARD 8 (to 36)
...

В глобальном коде используются инструкции LOAD_NAME/STORE_NAME, в локальном LOAD_FAST/STORE_FAST. Других отличий в теле цикла нет.

Глобальная переменная видна в других модулях и может быть изменена из других модулей. Всякий раз когда интерпретатор читает её он обращается к словарю globals (он содержит все символы данного модуля) и получает значение переменной по её имени. При записи тоже надо отыскать переменную по имени и обновить её значение.

Напротив, локальная переменная не видна нигде кроме своей родной функции. Она не может быть изменена другим кодом по имени. Интерпретатор оптимизирует доступ к ней: он хранит все локальные переменные в массиве и обращается к ним по индексам.

Пара LOAD_NAME/STORE_NAME работает со словарём globals. Пара LOAD_FAST/STORE_FAST работает с массивом, который условно можно назвать locals. Обращения к массиву быстрее, локальные инструкции быстрее, что делает локальный код в данном случае в два раза быстрее.

P.S. В других языках доступ к глобальным переменным оптимизируется аналогично доступу к локальным. Для этого нужно что набор глобальных имён был бы неизменным во время работы программы. Тогда их можно перечислить при компиляции, каждому имени сопоставить индекс и работать с массивом вместо словаря. Но в Питоне модули позволяют добавлять и удалять символы во время работы программы, а это затрудняет оптимизацию. С другой стороны в функциях состав переменных постоянен, что позволяет работать быстро.

→ Ссылка