小编典典

为什么使用中间变量的代码要比不使用中间变量的代码快?

python

我遇到这种奇怪的行为,但无法解释。这些是基准:

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%)


阅读 305

收藏
2021-01-20

共1个答案

小编典典

我的结果与您的结果相似:使用中间变量的代码在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区域,其withoutvarsmmap/ 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_ArenaMmapObjects/obmalloc.c;
obmalloc.c还包含宏ARENA_SIZE,这是#defined至是(256 << 10)(即262144);
类似地munmap匹配_PyObject_ArenaMunmapfrom 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,这是因为他们的系统调用相对昂贵的-而且,mmapMAP_ANONYMOUS要求新映射的页面必须清零-
尽管Python的也不会在意。

该行为在使用中间变量的代码中不存在,因为它使用了 更多的
内存,并且由于仍在其中分配了一些对象,因此无法释放任何内存空间。那是因为timeit它将使其循环成环

for n in range(10000)
    a = tuple(range(2000))
    b = tuple(range(2000))
    a == b

现在的行为是,无论ab将保持约束,直到他们重新分配*,所以在第二次迭代,tuple(range(2000))将分配一个3元组,并分配a = tuple(...)将降低旧的元组的引用计数,导致它被释放,并提高新元组的引用计数;然后发生同样的事情b。因此,在第一次迭代之后,这些元组中始终至少有2个(如果不是3个),因此不会发生颠簸。

最值得注意的是,不能保证使用中间变量的代码总是更快-实际上,在某些设置中,使用中间变量可能会导致额外的mmap调用,而直接比较返回值的代码可能没问题。


有人问为什么timeit禁用垃圾收集时会发生这种情况。确实是这样timeit做的

注意

默认情况下,timeit()在计时期间临时关闭垃圾收集。这种方法的优势在于,它使独立计时更具可比性。这个缺点是GC可能是被测功能性能的重要组成部分。如果是这样,则可以将GC作为设置字符串中的第一条语句重新启用。例如:

但是,Python的垃圾收集器仅用于回收 循环垃圾 ,即引用形成循环的对象的集合。这里不是这种情况。而是当引用计数降至零时立即释放这些对象。

2021-01-20