我遇到这种奇怪的行为,但无法解释。这些是基准:
py -3 -m timeit "tuple(range(2000)) == tuple(range(2000))" 10000 loops, best of 3: 97.7 usec per loop py -3 -m timeit "a = tuple(range(2000)); b = tuple(range(2000)); a==b" 10000 loops, best of 3: 70.7 usec per loop
与使用变量分配进行比较,为什么比使用带有临时变量的班轮快27%以上呢?
通过Python文档,垃圾回收在timeit期间被禁用,因此并非如此。这是某种优化吗?
结果也可以在Python 2.x中重现,尽管程度较小。
运行Windows 7,CPython 3.5.1,Intel i7 3.40 GHz,64位OS和Python。似乎我尝试使用Python 3.5.0在Intel i7 3.60 GHz上运行的另一台机器无法重现结果。
使用具有timeit.timeit()10000个循环的相同Python进程运行分别产生0.703和0.804。仍然显示,尽管程度较小。(〜12.5%)
timeit.timeit()
我的结果与您的结果相似:使用中间变量的代码在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 。
-mtimeit
因此,这个Heisenbug变得非常有趣。我决定运行该命令,strace确实发生了一些麻烦:
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倍。
mmap
对于256k区域,其withoutvars满mmap/ munmap。这些相同的行一遍又一遍地重复:
withoutvars
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,这是#defined至是(256 << 10)(即262144); 类似地munmap匹配_PyObject_ArenaMunmapfrom obmalloc.c。
_PyObject_ArenaMmap
Objects/obmalloc.c
obmalloc.c
ARENA_SIZE
#define
(256 << 10)
262144
_PyObject_ArenaMunmap
obmalloc.c 说
在Python 2.5之前,竞技场从未被使用过free()。从Python 2.5开始,我们确实尝试使用free()竞技场,并使用一些温和的启发式策略来增加最终释放竞技场的可能性。
free()
因此,这些试探法以及Python对象分配器在清空后立即释放这些空闲区域的事实导致python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))'触发病理行为,其中一个256 kiB内存区域被重新分配并重复释放。这种分配情况与mmap/ munmap,这是因为他们的系统调用相对昂贵的-而且,mmap与MAP_ANONYMOUS要求新映射的页面必须清零- 尽管Python的也不会在意。
python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))'
MAP_ANONYMOUS
该行为在使用中间变量的代码中不存在,因为它使用了 更多的 内存,并且由于仍在其中分配了一些对象,因此无法释放任何内存空间。那是因为timeit它将使其循环成环
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个),因此不会发生颠簸。
a
b
tuple(range(2000))
a = tuple(...)
最值得注意的是,不能保证使用中间变量的代码总是更快-实际上,在某些设置中,使用中间变量可能会导致额外的mmap调用,而直接比较返回值的代码可能没问题。
有人问为什么timeit禁用垃圾收集时会发生这种情况。确实是这样timeit做的:
注意 默认情况下,timeit()在计时期间临时关闭垃圾收集。这种方法的优势在于,它使独立计时更具可比性。这个缺点是GC可能是被测功能性能的重要组成部分。如果是这样,则可以将GC作为设置字符串中的第一条语句重新启用。例如:
注意
默认情况下,timeit()在计时期间临时关闭垃圾收集。这种方法的优势在于,它使独立计时更具可比性。这个缺点是GC可能是被测功能性能的重要组成部分。如果是这样,则可以将GC作为设置字符串中的第一条语句重新启用。例如:
timeit()
但是,Python的垃圾收集器仅用于回收 循环垃圾 ,即引用形成循环的对象的集合。这里不是这种情况。而是当引用计数降至零时立即释放这些对象。