差异确实如此greatly取决于“do stuff”实际做了什么并且mainly关于它访问定义/使用的名称的次数。尽管代码相似,但这两种情况之间存在根本区别:
- 在函数中,用于加载/存储名称的字节代码是通过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_FAST
偏移量处的命令28
and 32
,这些用于访问b
中使用的名称BINARY_MULTIPLY
操作并存储z
分别是名称。正如它们的字节码名称所暗示的那样,他们是快速版本 of the LOAD_*/STORE_*
family.
模块和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
, which,如前所述,执行的命令更慢.
在这种情况下,执行时间会有明显的差异,主要是因为Python必须评估LOAD_NAME/STORE_NAME
and LOAD_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
将执行计时为函数:
start_time = time.time()
print(main())
print("Time: ", time.time() - start_time)
200
Time: 0.08665871620178223
If you time
在较小的循环中range
(例如for i in range(1000)
)您会注意到“模块”版本更快。发生这种情况是因为需要调用函数带来的开销main()
比引入的更大*_FAST
vs *_NAME
差异。因此,这在很大程度上与完成的工作量有关。
因此,真正的罪魁祸首以及这种差异如此明显的原因是for
使用循环。你一般有0
有理由在脚本的顶层放置一个像这样的密集循环。将其移至函数中并避免使用全局变量,它的设计目的是提高效率。
您可以查看每个字节码执行的代码。我将链接源3.5尽管我很确定Python的版本在这里2.7差别不大。字节码评估完成于Python/ceval.c具体在功能上PyEval_EvalFrameEx:
-
LOAD_FAST source - STORE_FAST source
-
LOAD_NAME source - STORE_NAME source
正如您将看到的,*_FAST
字节码只是使用 a 来获取存储/加载的值fastlocals框架对象内包含的局部符号表.