全局变量与局部变量的性能

Posted

技术标签:

【中文标题】全局变量与局部变量的性能【英文标题】:Performance with global variables vs local 【发布时间】:2012-09-17 09:21:44 【问题描述】:

我还是 Python 的新手,我一直在努力提高我的 Python 脚本的性能,所以我在使用和不使用全局变量的情况下对其进行了测试。我对它进行了计时,令我惊讶的是,它在声明全局变量而不是将本地变量传递给函数时运行得更快。这是怎么回事?我认为局部变量的执行速度更快? (我知道全局变量不安全,我还是很好奇。)

【问题讨论】:

【参考方案1】:

本地人应该更快

根据this page on locals and globals:

当一行代码询问变量 x 的值时,Python 会在所有可用的命名空间中按顺序搜索该变量:

本地命名空间 - 特定于当前函数或类方法。如果函数定义了一个局部变量 x,或者有一个参数 x,Python 将使用它并停止搜索。 全局命名空间 - 特定于当前模块。如果模块定义了一个名为 x 的变量、函数或类,Python 将使用它并停止搜索。 内置命名空间 - 对所有模块都是全局的。作为最后的手段,Python 将假定 x 是内置函数或变量的名称。

基于此,我假设局部变量通常更快。我的猜测是您所看到的内容与您的脚本有关。

本地人更快

这是一个使用局部变量的简单示例,在我的机器上大约需要 0.5 秒(Python 3 中为 0.3):

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

func()

还有全局版本,大约需要 0.7(Python 3 中为 0.5):

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 秒)。

更好的优化方法

如果你想优化你的程序,你可以做的最好的事情是profile it。这将告诉您什么花费的时间最多,因此您可以专注于此。你的流程应该是这样的:

    在开启分析的情况下运行您的程序。 查看 KCacheGrind 或类似程序中的配置文件以确定哪些函数花费的时间最多。 在这些函数中: 寻找可以缓存函数结果的地方(这样您就不必做太多工作)。 寻求算法改进,例如用封闭形式的函数替换递归函数,或用字典替换列表搜索。 重新配置文件以确保该功能仍然存在问题。 考虑使用multiprocessing。

【讨论】:

有趣。我为整个脚本的执行计时(我很难找出一个可能很奇怪的函数)。我在所有函数之外声明了 3 个全局变量,并且我没有使用任何循环。计时 7 次,差异很小,但使用全局变量时,它始终运行得更快。呵呵…… @janeh 7 次可能还不够。我在测试中使用了巨大的循环来获得任何一致的结果(它们仍然有±0.1左右的误差)。我在最后添加了一些信息,这些信息也可以帮助您进行性能调整。 谢谢,我已经放弃在这里使用正则表达式,这似乎加快了速度,我得看看下一步是否是多处理。去 globals 是一个愚蠢的想法,我想 :> 根据here 的讨论,这似乎不太正确。当 Python 代码被编译为字节码时,变量被识别为局部变量或全局变量,并调用适当的查找函数。访问全局变量需要更长的时间,因为它是一个字典,而局部变量基本上是一个数组,但据我所知,全局变量实际上并没有在局部变量中找不到变量的额外开销(可能在编译步骤期间除外)。跨度> 是的,但他问到关于将东西传递给函数的问题。你根本没有涉及到这一点【参考方案2】:

简单回答:

由于 Python 的动态特性,当解释器遇到像 abc 这样的表达式时,它会查找 a(首先尝试本地命名空间,然后是全局命名空间,最后是内置命名空间),然后查找该对象的名称空间来解析名称 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
>>> 

【讨论】:

【参考方案3】:

当 Python 编译一个函数时,该函数在调用它之前就知道其中的变量是局部变量、闭包还是全局变量。

我们有几种方法在函数中引用变量:

全局变量 关闭 当地人

让我们在几个不同的函数中创建这些类型的变量,以便我们自己查看:

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

在 Linux 上:

>>> import sys
>>> sys.version
'3.6.4 |Anaconda custom (64-bit)| (default, Mar 13 2018, 01:15:57) \n[GCC 7.2.0]'
>>> import timeit
>>> min(timeit.repeat(globalfoo))
0.08560040907468647
>>> min(timeit.repeat(closurefoo))
0.08592104795388877
>>> min(timeit.repeat(defaultfoo))
0.06587386003229767
>>> min(timeit.repeat(localfoo))
0.06887826602905989

我会添加其他系统,因为我有机会测试它们。

【讨论】:

【参考方案4】:

您未包括的时间是程序员在跟踪使用全局时创建的错误所花费的时间,这些错误会在您的程序的其他地方产生副作用。这个时间比创建和释放局部变量所花费的时间要多很多倍,

【讨论】:

以上是关于全局变量与局部变量的性能的主要内容,如果未能解决你的问题,请参考以下文章

局部变量 与 全局变量

python函数中局部变量与全局变量遵守规则

局部变量与全局变量

python:局部变量与全局变量

全局变量与局部变量

局部变量 静态局部变量 全局变量与静态局部变量