Python - 全局变量与本地变量的性能

jan*_*neh 25 python performance

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

Rei*_*ica 31

当地人应该更快

根据本地和全局的这个页面:

当一行代码询问变量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()
Run Code Online (Sandbox Code Playgroud)

而全球版本大约需要0.7(Python 3中为0.5):

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

func()
Run Code Online (Sandbox Code Playgroud)

global 对已经全局化的变量做了一些奇怪的事情

有趣的是,这个版本在0.8秒内运行:

global x
x = 5
for i in range(10000000):
    x = 5
Run Code Online (Sandbox Code Playgroud)

虽然这在0.9中运行:

x = 5
for i in range(10000000):
    x = 5
Run Code Online (Sandbox Code Playgroud)

您会注意到,在这两种情况下,x都是一个全局变量(因为没有函数),并且它们都比使用本地变慢.我不知道为什么global x在这种情况下声明帮助.

Python 3中不会出现这种奇怪现象(两个版本大约需要0.6秒).

更好的优化方法

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

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


sta*_*ark 10

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


Moh*_*sin 10

简单回答:

由于Python的动态特性,当解释器遇到类似abc的表达式时,它会查找(首先尝试本地命名空间,然后是全局命名空间,最后是内置命名空间),然后查找该对象的命名空间以解析名称b,最后它在该对象的命名空间中查找以解析名称c.这些查找速度相当快; 对于局部变量,查找非常快,因为解释器知道哪些变量是本地的,并且可以在内存中为它们分配已知位置.

解释器知道函数中的哪些名称是本地的,并在函数调用的内存中为它们分配特定的(已知的)位置.这使得对locals的引用比对于globals(尤其是)内置函数更快.

代码示例解释相同:

>>> 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
>>> 
Run Code Online (Sandbox Code Playgroud)


Aar*_*all 5

当Python编译一个函数时,如果函数中的变量是locals,closures或globals,则函数在调用之前就知道了.

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

  • 全局
  • 关闭
  • 当地人

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

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
Run Code Online (Sandbox Code Playgroud)

Dissassembled

我们可以看到每个函数都知道在哪里查找变量 - 它不需要在运行时这样做:

>>> 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
Run Code Online (Sandbox Code Playgroud)

我们可以看到,当前全局的字节码是LOAD_GLOBAL,闭包变量是LOAD_DEREF,而本地是LOAD_FAST.这些是CPython的实现细节,可能会随版本而变化 - 但是能够看到Python以不同方式处理每个变量查找是很有用的.

粘贴到翻译中并亲自查看:

import dis
dis.dis(globalfoo)
dis.dis(closurefoo)
dis.dis(defaultfoo)
dis.dis(localfoo)
Run Code Online (Sandbox Code Playgroud)

测试代码

测试代码(随意在您的系统上测试):

import sys
sys.version
import timeit
min(timeit.repeat(globalfoo))
min(timeit.repeat(closurefoo))
min(timeit.repeat(defaultfoo))
min(timeit.repeat(localfoo))
Run Code Online (Sandbox Code Playgroud)

产量

在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
Run Code Online (Sandbox Code Playgroud)

在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
Run Code Online (Sandbox Code Playgroud)

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