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

use*_*264 8 python optimization python-2.7 python-3.x

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

def main():
    [do stuff]
    return something
print(main())
Run Code Online (Sandbox Code Playgroud)

在python中运行速度比在顶层执行命令1.5x3x快得多:

[do stuff]
print(something)
Run Code Online (Sandbox Code Playgroud)

Jim*_*ard 14

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

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

功能和LOAD_FAST/STORE_FAST:

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

def main():
    b = 20
    for i in range(1000000): z = 10 * b 
    return z
Run Code Online (Sandbox Code Playgroud)

产生的输出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/]
Run Code Online (Sandbox Code Playgroud)

这里要注意的一点是LOAD_FAST/STORE_FAST指令的偏移2832,这些都是用来访问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/]
Run Code Online (Sandbox Code Playgroud)

在这里,我们有多个调用LOAD_NAME/STORE_NAME,如前所述,这些调用是更缓慢的命令.

在这种情况下,存在将是在执行时间具有明显的差异,主要是因为Python必须评估LOAD_NAME/STORE_NAMELOAD_FAST/STORE_FAST多次(由于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
Run Code Online (Sandbox Code Playgroud)

将执行时间定位为函数:

start_time = time.time()
print(main())
print("Time: ", time.time() - start_time)
200
Time:  0.08665871620178223 
Run Code Online (Sandbox Code Playgroud)

如果你time循环一个较小的range(例如for i in range(1000)),你会注意到'模块'版本更快.这是因为需要调用函数引入的开销main()大于*_FASTvs *_NAME差异引入的开销.所以它在很大程度上取决于完成的工作量.

所以,这里真正的罪魁祸首,以及这种差异显而易见的原因是使用的for循环.您通常0有理由在脚本的顶层放置类似于密集循环的密集循环.在函数中移动它并避免使用全局变量,它被设计为更高效.


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

正如您将看到的,*_FAST字节码只是使用fastlocals包含在框架对象内本地符号表来获取存储/加载的值.