我对 Python 还是很陌生,我一直在努力提高我的 Python 脚本的性能,所以我在使用和不使用全局变量的情况下对其进行了测试。我对它进行了计时,令我惊讶的是,它在声明全局变量而不是将局部变量传递给函数时运行得更快。这是怎么回事?我认为局部变量的执行速度更快?(我知道全局变量不安全,我仍然很好奇。)
4 回答
当地人应该更快
根据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 秒)。
更好的优化方法
如果你想优化你的程序,你能做的最好的事情就是分析它。这将告诉您什么花费的时间最多,因此您可以专注于此。你的过程应该是这样的:
- 运行您的程序并进行分析。
- 查看 KCacheGrind 或类似程序中的配置文件以确定哪些函数花费的时间最多。
- 在这些功能中:
- 寻找可以缓存函数结果的地方(这样您就不必做太多工作)。
- 寻找算法改进,例如用封闭形式的函数替换递归函数,或用字典替换列表搜索。
- 重新配置文件以确保该功能仍然存在问题。
- 考虑使用multiprocessing。
简单的答案:
由于 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
>>>
当 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
我将添加其他系统,因为我有机会测试它们。
您不包括的时间是程序员花费在跟踪使用全局时创建的错误的时间,这些错误会在您的程序的其他地方产生副作用。那个时间比创建和释放局部变量所花费的时间大很多倍,