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

32

我对Python还不是很熟悉,一直在尝试提高我的Python脚本的性能,所以我测试了使用全局变量和将本地变量传递给函数两种情况。我计时后发现,令我惊讶的是,在声明全局变量后脚本运行得比传递本地变量更快。这是怎么回事?我认为使用本地变量执行速度更快?(我知道全局变量不安全,但我还是很好奇。)

4个回答

42

本地变量应该更快

根据这个关于本地变量和全局变量的页面:

当一行代码要求变量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
@janeh,7次可能还不够。我在我的测试中使用了巨大的循环来获得任何一致的结果(它们仍然有约±0.1的误差)。我在结尾处添加了一些信息,这也可能对您的性能调优有所帮助。 - Brendan Long
谢谢,我已经放弃在这里使用正则表达式了,这似乎加快了速度,我需要看看多进程是否是下一步。全局变量是一个愚蠢的想法,我想:> - janeh
根据这里的讨论,似乎并不完全正确。当Python代码被编译成字节码时,变量被标识为局部或全局,并调用相应的查找函数。访问全局变量需要更长的时间,因为它是一个字典,而局部变量基本上是一个数组,但据我所知,全局变量实际上没有在局部变量中找不到变量的额外开销(也许只有在编译步骤中)。 - seaotternerd
是的,但他问了关于向函数传递参数的问题.. 你完全没有涉及到那个。 - john k

18

简单回答:

由于Python的动态性,在遇到像a.b.c这样的表达式时,解释器会首先查找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
>>> 

16

当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

我在有机会测试其他系统时会将它们添加进来。


10
你不包括的时间是程序员花费在追踪错误上的时间,这些错误是由于在程序的其他地方使用全局变量所导致的副作用造成的。这个时间比创建和释放局部变量所花费的时间多很多。请注意,保留了 HTML 标签。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接