我的结果与您的结果相似:使用中间变量的代码在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 。
因此,这个Heisenbug变得非常有趣。我决定运行该命令,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
系统调用比使用中间变量的代码多近1000倍。
对于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
通话似乎是从功能来_PyObject_ArenaMmap
从Objects/obmalloc.c
; 在obmalloc.c
还包含宏ARENA_SIZE
,这是#define
d至是(256 << 10)
(即262144
); 类似地munmap
匹配_PyObject_ArenaMunmap
from obmalloc.c
。
obmalloc.c
说
在Python 2.5之前,竞技场从未被使用过free()
。从Python 2.5开始,我们确实尝试使用free()
竞技场,并使用一些温和的启发式策略来增加最终释放竞技场的可能性。
因此,这些启发式方法以及Python对象分配器在清空后立即释放这些免费区域的事实导致 python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))'
触发病理行为,其中一个256 kiB内存区域被重新分配并重复释放。这种分配情况与mmap
/ munmap
,这是因为他们的系统调用相对昂贵的-而且,mmap
与MAP_ANONYMOUS
要求新映射的页面必须清零-尽管Python的也不会在意。
该行为在使用中间变量的代码中不存在,因为它使用了一些 更多的内存,并且由于仍在其中分配了一些对象,因此无法释放任何内存空间。那是因为timeit
它将使其循环成环
for n in range(10000)
a = tuple(range(2000))
b = tuple(range(2000))
a == b
现在的行为是 a
和b
将保持约束,直到他们重新分配*,所以在第二次迭代,tuple(range(2000))
将分配一个3元组,并分配a = tuple(...)
将降低旧的元组的引用计数,导致它被释放,并提高新元组的引用计数;然后发生同样的事情b
。因此,在第一次迭代之后,这些元组中始终至少有2个(如果不是3个),因此不会发生颠簸。
最值得注意的是,不能保证使用中间变量的代码总是更快-实际上,在某些设置中,使用中间变量可能会导致额外的mmap
调用,而直接比较返回值的代码可能没问题。
有人问为什么timeit
禁用垃圾收集时会发生这种情况。确实timeit
做到了:
注意
默认, timeit()
在计时期间临时关闭垃圾收集。这种方法的优势在于,它使独立计时更具可比性。这个缺点是GC可能是被测功能性能的重要组成部分。如果是这样,则可以将GC作为设置字符串中的第一条语句重新启用。例如:
但是,Python的垃圾收集器仅用于回收循环垃圾,即引用形成循环的对象的集合。这里不是这种情况。而是当引用计数降至零时立即释放这些对象。
dis.dis("tuple(range(2000)) == tuple(range(2000))")
到dis.dis("a = tuple(range(2000)); b = tuple(range(2000)); a==b")
。在我的配置中,第二个片段实际上包含了第一个片段的所有字节码以及一些其他指令。很难相信更多的字节码指令会导致更快的执行速度。也许是特定Python版本中的某些错误?