31

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

4

4 回答 4

35

当地人应该更快

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

更好的优化方法

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

  1. 运行您的程序并进行分析。
  2. 查看 KCacheGrind 或类似程序中的配置文件以确定哪些函数花费的时间最多。
  3. 在这些功能中:
    • 寻找可以缓存函数结果的地方(这样您就不必做太多工作)。
    • 寻找算法改进,例如用封闭形式的函数替换递归函数,或用字典替换列表搜索。
    • 重新配置文件以确保该功能仍然存在问题。
    • 考虑使用multiprocessing
于 2012-09-25T20:00:51.110 回答
16

简单的答案:

由于 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
>>> 
于 2013-11-25T19:33:39.887 回答
14

当 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

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

于 2017-10-17T20:41:09.337 回答
9

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

于 2012-09-25T19:58:00.923 回答