1 2 3 4
| def main():
for i in xrange(10**8):
pass
main() |
python中的这段代码运行在中(注意:计时是通过Linux中bash中的time函数完成的)。
1 2 3
| real 0m1.841s
user 0m1.828s
sys 0m0.012s |
但是,如果for循环不在函数中,
1 2
| for i in xrange(10**8):
pass |
然后它会运行更长的时间:
1 2 3
| real 0m4.543s
user 0m4.524s
sys 0m0.012s |
为什么会这样?
- 你是怎么安排时间的?
- 已确认python 3.2.3 repl的行为。有趣。
- 只是一种直觉,不确定它是否是真的:我想这是因为范围。在函数的情况下,将创建一个新的作用域(即一种散列,变量名绑定到其值)。如果没有函数,变量就在全局范围内,这时可以找到很多东西,从而减慢循环速度。
- 直到我复制了这个我才相信你。Python 2.7.2 (default, Jun 12 2011, 15:08:59) [MSC v.1500 32 bit (Intel)] on win32
- @谢伦,似乎不是这样。在不明显影响运行时间的情况下,将200k个虚拟变量定义到作用域中。
- 有趣的是…也在OSXLion上用python 2.7.2复制了它。2.2秒对4.2秒。
- 亚历克斯·马泰利就这个stackoverflow.com/a/1813167/174728写了一个很好的答案。
- @你说得对。这是关于范围的,但是在本地更快的原因是本地范围实际上是作为数组而不是字典实现的(因为它们的大小在编译时是已知的)。
- @andrewjaffe输出建议使用linux的time命令。
- @AndrewJaffeWardMuylaert是正确的,我在bash中使用了time命令。我现在把这个额外的细节包括在这个问题中。
- 我刚刚在ipython 2.7.5%timeit"def main():for i in xrange(108):pass;main()"=>100000000个循环中测试了这段代码,每个循环中最好3:16.9ns和%timeit"for i in xrange(108):pass"=>100000000个循环,每个循环中最好3:16.6ns
在函数内部,字节码是
1 2 3 4 5 6 7 8 9 10 11 12
| 2 0 SETUP_LOOP 20 (to 23)
3 LOAD_GLOBAL 0 (xrange)
6 LOAD_CONST 3 (100000000)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 6 (to 22)
16 STORE_FAST 0 (i)
3 19 JUMP_ABSOLUTE 13
>> 22 POP_BLOCK
>> 23 LOAD_CONST 0 (None)
26 RETURN_VALUE |
在顶层,字节码是
1 2 3 4 5 6 7 8 9 10 11 12
| 1 0 SETUP_LOOP 20 (to 23)
3 LOAD_NAME 0 (xrange)
6 LOAD_CONST 3 (100000000)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 6 (to 22)
16 STORE_NAME 1 (i)
2 19 JUMP_ABSOLUTE 13
>> 22 POP_BLOCK
>> 23 LOAD_CONST 2 (None)
26 RETURN_VALUE |
区别在于STORE_FAST更快!比STORE_NAME还要多。这是因为在一个函数中,i是本地的,但在顶层它是全局的。
要检查字节码,请使用dis模块。我可以直接反汇编这个函数,但要反汇编顶层代码,我必须使用compile内建。
- 实验证实。在main函数中插入global i使运行时间相等。
- 这回答了问题,但没有回答问题:)在局部函数变量的情况下,cpython实际上将这些变量存储在一个元组(从C代码中是可变的)中,直到请求字典为止(例如通过locals()或inspect.getframe()等)。用常量整数查找数组元素比搜索dict快得多。
- 与C/C++同样,使用全局变量会导致显著的减速。
- 这是我第一次看到字节码。一个人如何看待它,并且知道这一点很重要?
- 只有当你想知道为什么一件事比另一件事更快,或者你打算修改或扩展cpython时。
- @编码干扰:情况怎么样?
- 函数中的变量往往位于堆栈的顶部,从而提供更快的访问速度。全局变量通常位于堆中或父段之外,具体取决于编译器实现。因此访问它们的速度较慢(stackoverflow.com/questions/1169858/…)
- @编码干扰机这是非常依赖系统的。在一个相对"哑"的微处理器上,它不关心内存在RAM中的位置。
- @我同意,它与计算机体系结构密切相关。不过,我注意到,我们拥有的通用计算机就是这样。
- @码干扰器足够公平。这毕竟是一个关于python的问题(尽管它确实在一些微处理器上运行)。只是不想有人对这一点过度概括。
- @我同意。只是想分享两件事i)这种行为在其他编程语言中也有体现i i)因果因素更多的是体系结构方面,而不是语言本身的真正意义。
- 如何读取字节码,从何处可以获取函数的字节码?
- @pratik请参阅dis模块的文档:docs.python.org/library/dis.html
- @ecatmur您能告诉我如何使用compile内置的反汇编顶级代码吗?
- @manty写dis.dis(compile('for i in xrange(10**8):
pass', 'main.py', 'exec'))。
您可能会问为什么存储局部变量比存储全局变量更快。这是一个cpython实现细节。
请记住,cpython被编译为字节码,解释器将运行它。编译函数时,局部变量存储在固定大小的数组中(不是dict),变量名分配给索引。这是可能的,因为您不能动态地向函数添加局部变量。然后,检索一个局部变量实际上是一个指向列表的指针查找,并且在PyObject上增加了refcount,这是很简单的。
与全局查找(LOAD_GLOBAL相比,这是一个真正的dict搜索,包含哈希等等。顺便说一句,这就是为什么如果您希望global i是全局的,那么您需要指定它:如果您曾经在一个作用域内分配一个变量,那么编译器将发出STORE_FAST来访问它,除非您告诉它不要这样做。
顺便说一下,全球搜索仍然相当优化。属性查询foo.bar是非常慢的查询!
下面是局部可变效率的小例子。
- 这也适用于Pypy,直到当前版本(本文撰写时为1.8)。与函数内部相比,OP中的测试代码在全局范围内的运行速度大约慢了四倍。
- 这与JavaScript范围解析视频中提到的类似。我不认为全局变量查找比局部变量属性查找更快。今天学到了新东西!
- @沃克诺,他们不是,除非你倒着说。根据KatrielAlex和Ecatmur的说法,由于存储方法的原因,全局变量查找比局部变量查找慢。
- @Jeremypridemore,KatrielAlex'x答案的最后一句话是"顺便说一句,全球搜索仍然相当乐观。属性查找foo.bar是非常慢的!"他不是说全局查找是优化的和快速的,但是属性查找是非常缓慢的吗?
- @这里的主要对话是比较函数中的局部变量查找和模块级定义的全局变量查找。如果你在最初的评论回复中注意到这个答案,你会说:"我不认为全局变量查找比局部变量属性查找更快。"但事实并非如此。KatrielAlex说,虽然局部变量的查找比全局变量的查找快,但即使是全局变量的查找也比属性查找(这是不同的)要优化和快。我没有足够的空间发表更多评论。
- @杰里米普利德莫尔,对不起,我还是看不出我说的和他说的有什么区别。如果foo是本地的,而bar是全局的,那么访问foo.attr的速度要比bar快,这与我在初始注释中键入的内容一致。
- @walkereno foo.bar不是本地访问。它是一个对象的属性。(请原谅没有格式化)def foo_func: x = 5,x是函数的局部。访问x是本地的。foo = SomeClass(),foo.bar为属性访问。val = 5global是全球性的。关于速度局部>全局>属性,根据我在这里读到的内容。所以访问foo_func中的x是最快的,其次是val,其次是foo.bar。foo.attr不是本地查找,因为在这个保护的上下文中,我们谈论的本地查找是对属于函数的变量的查找。
- 对的。局部变量查找是一个固定时间指针。全局查找是一个dict搜索,但是有了优化(例如,我认为dict代码是内联的)。属性查找只是一个简单的python dict搜索。
- @沃克诺啊,我想我知道困惑可能在哪里。像foo.bar这样的属性查找与查找名为"foo.bar"的变量不同,它首先查找名为foo的(局部)变量,然后在foo.__dict__中搜索"bar"。这是第二个需要时间的部分。
- 这只是吹毛求疵,但指数的复数是指数,而不是指数。但不管怎样,是否有文档说明全局变量存储在dict中?另一个答案链接到用于存储本地变量的文档,其中说"将TOS存储到本地co-varnames[var-num]。"我不知道TOS的含义,但这可能意味着变量存储在数组中。但是在存储名称的文档中,不清楚是否使用了dict。另外,我对cpython一无所知,所以所有这些信息都适用于普通的python吗?
- @Octar查看了globals()函数。如果您想要更多的信息,您可能需要开始查看Python的源代码。而cpython只是Python通常实现的名称——所以您可能已经在使用它了!
- @凯特里·亚历克斯谢谢,我可能会这么做的。
- @八分之一:实际上,在美国英语中,indexes和indices都是index复数的有效拼写。你可以在任何字典里查到。几个流行的例子:merriam webster,dictionary.com
- @约翰尼:买我用英国英语!我是澳大利亚人!另外,KatrielAlex也是英国人(看看他/她的个人主页)。:)
- @我没有发现任何迹象表明索引在任何英语方言中是无效或不正确的索引复数。许多人喜欢指数,特别是在数学或技术背景下,但这绝不会使指数出错。请参阅english.stackexchange.com/questions/61080/indexes-or-indexes和许多其他参考资料。
- @约翰尼:那我的错。我只是假设索引必须是正确的版本,因为它是我见过的唯一一个。
- 当你们谈论"属性查找"foo.bar速度慢时,这是否包括在类的方法中使用self.my_var?如果是这样,那么为什么人们总是在类中使用属性查找而不只是使用一个巨大的全局字典呢?
- 是的,因为大多数情况下,轻微的慢度实际上对程序的运行速度并不重要。
- "这是可能的,因为您不能动态地向函数添加局部变量。"那么locals()[dynamic_expr] = val呢?
- @Quelklef:Modifing the dictreturned by localsis explicited noted to be unsupported;it can't actually add"real"locals,but the implementation is such that the only way you could dynally check said"locals"will happen work,sometimes,on the CPTHON interpreter;when you first called 2 in a function一个现实的地方像一个EDOCX1〕〔0〕的镜子,隐藏它;如果你叫locals()〔2〕同样的功能,你也会得到同样的dict。但事实上,这并不是一个"真实"的地方阵列。
- @Shadowranger Fair Point.
除了本地/全局变量存储时间外,操作码预测使函数更快。
正如其他答案所解释的,函数在循环中使用STORE_FAST操作码。下面是函数循环的字节码:
1 2 3
| >> 13 FOR_ITER 6 (to 22) # get next value from iterator
16 STORE_FAST 0 (x) # set local variable
19 JUMP_ABSOLUTE 13 # back to FOR_ITER |
通常,当程序运行时,python一个接一个地执行每个操作码,跟踪一个堆栈,并在每个操作码执行后在堆栈帧上执行其他检查。操作码预测意味着在某些情况下,Python能够直接跳到下一个操作码,从而避免了一些开销。
在这种情况下,每次python看到EDOCX1(循环的顶部),它都会"预测"STORE_FAST是它必须执行的下一个操作码。然后,python偷看下一个操作码,如果预测正确,它直接跳到STORE_FAST。这会将两个操作码压缩为一个操作码。
另一方面,在全局级别的循环中使用STORE_NAME操作码。当python看到这个操作码时,它不会做出类似的预测。相反,它必须回到评估循环的顶部,这对循环执行的速度有明显的影响。
为了提供关于这种优化的更多技术细节,这里引用了ceval.c文件(python虚拟机的"引擎"):
Some opcodes tend to come in pairs thus making it possible to
predict the second code when the first is run. For example,
GET_ITER is often followed by FOR_ITER. And FOR_ITER is often
followed by STORE_FAST or UNPACK_SEQUENCE.
Verifying the prediction costs a single high-speed test of a register
variable against a constant. If the pairing was good, then the
processor's own internal branch predication has a high likelihood of
success, resulting in a nearly zero-overhead transition to the
next opcode. A successful prediction saves a trip through the eval-loop
including its two unpredictable branches, the HAS_ARG test and the
switch-case. Combined with the processor's internal branch prediction,
a successful PREDICT has the effect of making the two opcodes run as if
they were a single new opcode with the bodies combined.
我们可以在FOR_ITER操作码的源代码中看到STORE_FAST的预测位置:
1 2 3 4 5 6 7 8 9 10
| case FOR_ITER: // the FOR_ITER opcode case
v = TOP();
x = (*v->ob_type->tp_iternext)(v); // x is the next value from iterator
if (x != NULL) {
PUSH(x); // put x on top of the stack
PREDICT(STORE_FAST); // predict STORE_FAST will follow - success!
PREDICT(UNPACK_SEQUENCE); // this and everything below is skipped
continue;
}
// error-checking and more code for when the iterator ends normally |
PREDICT函数扩展到if (*next_instr == op) goto PRED_##op,即跳转到预测操作码的起始处。在这种情况下,我们跳到这里:
1 2 3 4 5
| PREDICTED_WITH_ARG(STORE_FAST);
case STORE_FAST:
v = POP(); // pop x back off the stack
SETLOCAL(oparg, v); // set it as the new local variable
goto fast_next_opcode; |
本地变量现在设置好,下一个操作码就可以执行了。python继续通过iterable,直到它到达末尾,每次都进行成功的预测。
python wiki页面提供了有关cpython的虚拟机如何工作的更多信息。
- Minor Update:as of CPYTHON 3.6,the savings from prediction go down a bit;instead of two undepredictable brains,there is only one.这一变化是由于从Bytecode到Wordcode的开关;现在所有的"wordcode s"都有一个论据,只是在指令不符合逻辑的情况下才出现。测试从来没有出现过(除非低水平的跟踪在编译和运行中同时出现,没有正常的建筑物),只有一次不可预测的跳跃。
- 即使无法预测的跳跃不会发生在CPYTHON的大部分建筑物,因为新建筑物(如Python 3.1,由Default在3.2中创建)Computed Gotos Behavior;when used,the PREDICTMacro is completely disabled;Instead most cases end in a EDOCX1〕〔8〕。但在分支预测CPUS中,效果与PREDICT相似,因为分支(和预测)是每一个操作代码,增加了成功分支预测的范围。