Python - performance with global variables vs local
我对python还是个新手,我一直在努力提高python脚本的性能,所以我在使用和不使用全局变量的情况下对它进行了测试。我对它计时,令我惊讶的是,它使用声明的全局变量运行得更快,而不是将局部变量传递给函数。发生什么事?我认为局部变量的执行速度更快?(我知道全球不安全,我仍然好奇。)
当地人应该更快
根据本页关于本地和全球的内容:
When a line of code asks for the value of a variable x, Python will search for that variable in all the available namespaces, in order:
- local namespace - specific to the current function or class method. If the function defines a local variable x, or has an argument x, Python will use this and stop searching.
- global namespace - specific to the current module. If the module has defined a variable, function, or class called x, Python will use that and stop searching.
- built-in namespace - global to all modules. As a last resort, Python will assume that x is the name of built-in function or variable.
基于此,我假设局部变量通常更快。我猜你看到的是你剧本中的一些特别之处。
当地人更快下面是一个使用局部变量的简单示例,它在我的机器上大约需要0.5秒(在Python3中为0.3秒):
1 2 3 4 5 | def func(): for i in range(10000000): x = 5 func() |
而全局版本,大约需要0.7(在python3中是0.5):
1 2 3 4 5 6 | def func(): global x for i in range(1000000): x = 5 func() |
有趣的是,这个版本在0.8秒内运行:
1 2 3 4 | global x x = 5 for i in range(10000000): x = 5 |
当它在0.9中运行时:
1 2 3 | x = 5 for i in range(10000000): x = 5 |
您会注意到,在这两种情况下,
这种奇怪在Python3中不会发生(两个版本都需要大约0.6秒)。
更好的优化方法如果你想优化你的程序,你能做的最好的事情就是分析它。这将告诉你最花时间的是什么,所以你可以集中精力。你的过程应该是这样的:
- 寻找可以缓存函数结果的地方(这样就不必做太多的工作)。
- 寻找算法上的改进,比如用关闭的表单函数替换递归函数,或者用字典替换列表搜索。
- 重新分析以确保函数仍然存在问题。
- 考虑使用多处理。
简单回答:
由于python的动态特性,当解释器遇到类似a.b.c的表达式时,它会查找a(首先尝试本地名称空间,然后尝试全局名称空间,最后是内置名称空间),然后在该对象的名称空间中查找以解析名称b,最后在该对象的名称空间中查找以解析名称c。这些lookup相当快;对于局部变量,查找速度非常快,因为解释器知道哪些变量是局部的,并且可以在内存中为它们分配一个已知的位置。
解释器知道函数中的哪些名称是本地的,并在函数调用的内存中为它们分配特定的(已知的)位置。这使得对本地的引用比对全局的引用快得多,尤其是对内置的引用。
代码示例解释相同的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | >>> 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编译函数时,函数在被调用之前就知道函数中的变量是局部变量、闭包还是全局变量。
我们在函数中有几种引用变量的方法:
- 全局变量
- 闭包
- 当地人
那么让我们在几个不同的函数中创建这些类型的变量,这样我们就可以自己看到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 |
拆装
我们可以看到,每个函数都知道在哪里查找变量-它不需要在运行时这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | >>> 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 |
我们可以看到,当前全局的字节码是
粘贴到解释器中,自己查看:
1 2 3 4 5 | import dis dis.dis(globalfoo) dis.dis(closurefoo) dis.dis(defaultfoo) dis.dis(localfoo) |
测试代码
测试代码(请随意在系统上测试):
1 2 3 4 5 6 7 | import sys sys.version import timeit min(timeit.repeat(globalfoo)) min(timeit.repeat(closurefoo)) min(timeit.repeat(defaultfoo)) min(timeit.repeat(localfoo)) |
产量
在Windows上,至少在这个版本中,闭包看起来会受到一点惩罚——使用默认的本地闭包是最快的,因为您不必每次都分配本地闭包:
1 2 3 4 5 6 7 8 9 10 11 12 | >>> 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上:
1 2 3 4 5 6 7 8 9 10 11 12 13 | >>> import sys >>> sys.version '3.6.4 |Anaconda custom (64-bit)| (default, Mar 13 2018, 01:15:57) [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 |
我将添加其他系统,因为我有机会测试它们。