我的结果与你的类似:在Python 3.4中,使用中间变量的代码始终至少快10-20%.然而,当我在同一个Python 3.4解释器上使用IPython时,我得到了以下结果:
In [1]: %timeit -n10000 -r20 tuple(range(2000)) == tuple(range(2000))
10000 loops, best of 20: 74.2 µs per loop
In [2]: %timeit -n10000 -r20 a = tuple(range(2000)); b = tuple(range(2000)); a==b
10000 loops, best of 20: 75.7 µs per loop
值得注意的是,当我在命令行中使用-mtimeit
时,我从未设法接近前者的74.2µs.
所以这只海森堡是很有趣的东西.我决定用strace
来运行这个命令,确实有些可疑之处:
% strace -o withoutvars python3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 134 usec per loop
% strace -o withvars python3 -mtimeit "a = tuple(range(2000)); b = tuple(range(2000)); a==b"
10000 loops, best of 3: 75.8 usec per loop
% grep mmap withvars|wc -l
46
% grep mmap withoutvars|wc -l
41149
这就是造成这种差异的一个很好的原因.不使用变量的代码导致调用mmap
系统调用的次数几乎是使用中间变量的调用次数的mmap
0倍.
对于一个256k的区域来说,withoutvars
是mmap
/munmap
;这些同样的台词一遍又一遍地重复:
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144) = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144) = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144) = 0
mmap
调用似乎来自Objects/obmalloc.c
的函数_PyObject_ArenaMmap
;obmalloc.c
还包含宏ARENA_SIZE
,其#define
d为(256 << 10)
(即262144
);同样,munmap
与obmalloc.c
中的_PyObject_ArenaMunmap
相匹配.
obmalloc.c
表示
在Python 2.5之前,竞技场从来都不是free()
'ed.从Python 2.5开始,
因此,这些启发式方法以及Python对象分配器在这些空闲区域清空后立即释放它们的事实导致python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))'
个触发病理行为,其中一个256kib的内存区域被重新分配并反复释放;这种分配发生在mmap
/munmap
上,这是相对昂贵的,因为它们是系统调用——此外,mmap
和MAP_ANONYMOUS
要求新映射的页面必须归零——尽管Python不在乎.
在使用中间变量的代码中不存在这种行为,因为它使用的内存略为more,并且无法释放内存,因为其中仍分配了一些对象.这是因为timeit
将使其成为一个循环,这与
for n in range(10000)
a = tuple(range(2000))
b = tuple(range(2000))
a == b
现在的行为是,a
和b
都将保持绑定,直到它们被*重新分配,因此在第二次迭代中,tuple(range(2000))
将分配第三个元组,分配a = tuple(...)
将减少旧元组的引用计数,导致其被释放,并增加新元组的引用计数;b
也一样.因此,在第一次迭代之后,这些元组中至少有2个元组,如果不是3个,那么就不会发生抖动.
最值得注意的是,不能保证使用中间变量的代码总是更快——事实上,在某些设置中,使用中间变量可能会导致额外的mmap
个调用,而直接比较返回值的代码可能会更好.
有人问为什么timeit
禁用垃圾收集时会发生这种情况.的确,timeit
does it:
Note
默认情况下,timeit()
会在计时期间临时关闭垃圾收集.这种方法的优点是,它使独立计时更具可比性.这个缺点是GC可能是被测量函数性能的重要组成部分.如果是这样,GC可以作为设置字符串中的第一条语句重新启用.例如:
然而,Python的垃圾收集器仅用于回收cyclic garbage个对象,即引用形成循环的对象集合.这里的情况并非如此;相反,当引用计数降至零时,这些对象会立即被释放.