Подтвердить что ты не робот

Python - производительность с глобальными переменными vs local

Я все еще новичок в Python, и я пытался улучшить производительность моего Python script, поэтому я тестировал его с глобальными переменными и без них. Я приурочил его, и, к моему удивлению, он работал быстрее с объявленными глобальными переменными, а не передавал локальные вары на функции. Что происходит? Я думал, что скорость выполнения была быстрее с локальными переменными? (Я знаю, что глобалы небезопасны, мне все еще интересно.)

4b9b3361

Ответ 1

Местные жители должны быть быстрее

В соответствии с этой страницей по локальным и глобальным значениям:

Когда строка кода запрашивает значение переменной x, Python будет искать эту переменную во всех доступных пространствах имен, чтобы:

  • локальное пространство имен - для текущей функции или метода класса. Если функция определяет локальную переменную x или имеет аргумент x, Python будет использовать это и прекратить поиск.
  • глобальное пространство имен - для текущего модуля. Если модуль определил переменную, функцию или класс с именем x, Python будет использовать это и прекратить поиск.
  • встроенное пространство имен - глобальное для всех модулей. В качестве последнего средства Python будет считать, что x является именем встроенной функции или переменной.

Исходя из этого, я бы предположил, что локальные переменные, как правило, быстрее. Мое предположение - это то, что вы видите, что-то особенное в вашем script.

Локали быстрее

Вот тривиальный пример с использованием локальной переменной, которая занимает около 0,5 секунды на моей машине (0,3 в Python 3):

def func():
    for i in range(10000000):
        x = 5

func()

И глобальная версия, которая занимает около 0.7 (0.5 в Python 3):

def func():
    global x
    for i in range(1000000):
        x = 5

func()

global делает что-то странное для переменных, которые уже являются глобальными

Интересно, что эта версия работает через 0,8 секунды:

global x
x = 5
for i in range(10000000):
    x = 5

Пока это работает в формате 0.9:

x = 5
for i in range(10000000):
    x = 5

Вы заметите, что в обоих случаях x является глобальной переменной (поскольку нет функций), и они медленнее, чем использование локальных. Я не знаю, почему объявление global x помогло в этом случае.

Эта странность не возникает в Python 3 (обе версии занимают около 0,6 секунды).

Лучшие методы оптимизации

Если вы хотите оптимизировать свою программу, самое лучшее, что вы можете сделать, это профиль. Это расскажет вам, что требуется больше времени, поэтому вы можете сосредоточиться на этом. Ваш процесс должен выглядеть примерно так:

  • Запустите программу с профилированием.
  • Посмотрите профиль в KCacheGrind или аналогичную программу, чтобы определить, какие функции занимают больше всего времени.
  • В этих функциях:
    • Найдите места, где вы можете кэшировать результаты функций (так что вам не нужно делать столько работы).
    • Ищите алгоритмические улучшения, такие как замена рекурсивных функций с помощью функций закрытой формы или замена поиска списка со словарями.
    • Повторный профиль, чтобы убедиться, что функция по-прежнему является проблемой.
    • Рассмотрите возможность использования multiprocessing.

Ответ 2

Время, в которое вы не включаете, - это время, затрачиваемое программистом на отслеживание ошибок, созданных при использовании глобального, имеет побочный эффект в другом месте вашей программы. Это время во много раз больше времени, затрачиваемого на создание и освобождение локальных переменных,

Ответ 3

Простой ответ:

Из-за динамической природы Python, когда интерпретатор сталкивается с выражением типа abc, он просматривает (сначала пытается использовать локальное пространство имен, затем глобальное пространство имен и, наконец, встроенное пространство имен), то он выглядит в этом объекте пространство имен для разрешения имени b, и, наконец, он ищет в этом пространстве имен объектов разрешение имени c. Эти поиски достаточно быстры; Для локальных переменных поиск выполняется очень быстро, поскольку интерпретатор знает, какие переменные являются локальными, и может назначить им известную позицию в памяти.

Интерпретатор знает, какие имена внутри ваших функций являются локальными, и назначает им определенные (известные) местоположения внутри памяти вызовов функций. Это делает ссылки на локальных жителей намного быстрее, чем на глобальные и, в особенности, на встроенные.

Пример кода, чтобы объяснить то же самое:

>>> glen = len # provides a global reference to a built-in
>>> 
>>> def flocal():
...     name = len
...     for i in range(25):
...         x = name
... 
>>> def fglobal():
...     for i in range(25):
...         x = glen
... 
>>> def fbuiltin():
...     for i in range(25): 
...         x = len
... 
>>> timeit("flocal()", "from __main__ import flocal")
1.743438959121704
>>> timeit("fglobal()", "from __main__ import fglobal")
2.192162036895752
>>> timeit("fbuiltin()", "from __main__ import fbuiltin")
2.259413003921509
>>> 

Ответ 4

Когда Python компилирует функцию, функция знает, прежде чем она вызывается, если переменные в ней являются локальными, закрывающими или глобальными.

У нас есть несколько способов ссылки на переменные в функциях:

  • Глобал
  • закрытие
  • locals

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

global_foo = 'foo'
def globalfoo():
    return global_foo

def makeclosurefoo():
    boundfoo = 'foo'
    def innerfoo():
        return boundfoo
    return innerfoo

closurefoo = makeclosurefoo()

def defaultfoo(foo='foo'):
    return foo

def localfoo():
    foo = 'foo'
    return foo

дизассемблировать

Мы видим, что каждая функция знает, где искать переменную - ей не нужно делать это во время выполнения:

>>> import dis
>>> dis.dis(globalfoo)
  2           0 LOAD_GLOBAL              0 (global_foo)
              2 RETURN_VALUE
>>> dis.dis(closurefoo)
  4           0 LOAD_DEREF               0 (boundfoo)
              2 RETURN_VALUE
>>> dis.dis(defaultfoo)
  2           0 LOAD_FAST                0 (foo)
              2 RETURN_VALUE
>>> dis.dis(localfoo)
  2           0 LOAD_CONST               1 ('foo')
              2 STORE_FAST               0 (foo)

  3           4 LOAD_FAST                0 (foo)
              6 RETURN_VALUE

Мы видим, что в настоящее время байт-код для глобального - это LOAD_GLOBAL, переменная замыкания LOAD_DEREF, а локальная - LOAD_FAST. Это детали реализации CPython и могут меняться от версии к версии, но полезно иметь возможность видеть, что Python обрабатывает каждый поиск переменных по-разному.

Вставьте в интерпретатор и убедитесь сами:

import dis
dis.dis(globalfoo)
dis.dis(closurefoo)
dis.dis(defaultfoo)
dis.dis(localfoo)

Тестовый код

Тестовый код (не стесняйтесь тестировать в вашей системе):

import sys
sys.version
import timeit
min(timeit.repeat(globalfoo))
min(timeit.repeat(closurefoo))
min(timeit.repeat(defaultfoo))
min(timeit.repeat(localfoo))

Выход

В Windows, по крайней мере в этой сборке, похоже, что закрытие получает немного штрафа - и использование локального, что по умолчанию является самым быстрым, потому что вам не нужно назначать локальный каждый раз:

>>> import sys
>>> sys.version
'3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]'
>>> import timeit
>>> min(timeit.repeat(globalfoo))
0.0728403456180331
>>> min(timeit.repeat(closurefoo))
0.07465484920749077
>>> min(timeit.repeat(defaultfoo))
0.06542038103088998
>>> min(timeit.repeat(localfoo))
0.06801849537714588

Я добавлю другие системы, поскольку у меня есть возможность их протестировать.