全局与本地命名空间性能差异

时间:2016-01-11 03:39:15

标签: python python-2.7 python-3.x optimization

为什么在函数中执行一组命令:

def main():
    [do stuff]
    return something
print(main())

在python中比在顶级执行命令更快地运行1.5x3x

[do stuff]
print(something)

1 个答案:

答案 0 :(得分:14)

差异确实极大取决于“做什么”实际做什么,主要取决于它访问定义/使用的名称的次数。假设代码类似,这两种情况之间存在根本区别:

  • 在函数中,加载/存储名称的字节代码使用 LOAD_FAST / STORE_FAST 完成。
  • 在顶级范围(即模块)中,使用 LOAD_NAME / STORE_NAME 执行相同的命令,这些命令更加缓慢。

在以下情况下可以查看,我将使用for循环来确保定义的变量的查找多次执行

功能和LOAD_FAST/STORE_FAST

我们定义了一个简单的函数来完成一些非常愚蠢的事情:

def main():
    b = 20
    for i in range(1000000): z = 10 * b 
    return z

dis.dis 生成的输出:

dis.dis(main)
# [/snipped output/]

             18 GET_ITER
        >>   19 FOR_ITER                16 (to 38)
             22 STORE_FAST               1 (i)
             25 LOAD_CONST               3 (10)
             28 LOAD_FAST                0 (b)
             31 BINARY_MULTIPLY
             32 STORE_FAST               2 (z)
             35 JUMP_ABSOLUTE           19
        >>   38 POP_BLOCK

# [/snipped output/]

此处需要注意的是偏移LOAD_FAST/STORE_FAST28上的32命令,这些命令用于访问b中使用的BINARY_MULTIPLY名称分别操作和存储z名称。正如他们的字节代码名称所暗示的那样,它们是LOAD_*/STORE_*系列的快速版本

模块和LOAD_NAME/STORE_NAME

现在,让我们看一下前一个函数的模块版本dis的输出:

# compile the module
m = compile(open('main.py', 'r').read(), "main", "exec")

dis.dis(m)
# [/snipped output/]

             18 GET_ITER
        >>   19 FOR_ITER                16 (to 38)
             22 STORE_NAME               2 (i)
             25 LOAD_NAME                3 (z)
             28 LOAD_NAME                0 (b)
             31 BINARY_MULTIPLY
             32 STORE_NAME               3 (z)
             35 JUMP_ABSOLUTE           19
        >>   38 POP_BLOCK

# [/snipped output/]

在这里,我们有多次调用LOAD_NAME/STORE_NAME ,如前所述,是执行的更缓慢的命令。

在这种情况下,执行时间会有明显的差异,主要是因为Python必须多次评估LOAD_NAME/STORE_NAMELOAD_FAST/STORE_FAST(由于{{ 1}}循环我添加了)因此,每次执行每个字节代码的代码时引入的开销将累积

将执行“作为模块”计时:

for

将执行时间定为函数:

start_time = time.time()
b = 20 
for i in range(1000000): z = 10 *b
print(z)
print("Time: ", time.time() - start_time)
200
Time:  0.15162253379821777

如果start_time = time.time() print(main()) print("Time: ", time.time() - start_time) 200 Time: 0.08665871620178223 循环使用较小的time(例如range),您会注意到“模块”版本更快。发生这种情况是因为需要调用函数for i in range(1000)引入的开销大于main()*_FAST差异引入的开销。所以它在很大程度上取决于完成的工作量。

所以,这里真正的罪魁祸首,以及这种差异显而易见的原因是使用了*_NAME循环。 您通常会for有理由在脚本的顶层放置一个类似于密集循环的密集循环。将其移入函数并避免使用全局变量,它的设计效率更高。

您可以查看为每个字节代码执行的代码。我会在这里链接3.5版本的Python的源代码,尽管我很确定2.7没有多大差异。字节码评估在Python/ceval.c中专门在函数PyEval_EvalFrameEx中完成:

正如您所看到的,0字节码只是使用fastlocals local symbol table contained inside frame objects获取存储/加载的值。