[英]Why is code using intermediate variables faster than code without?
我遇到了这种奇怪的行为并且无法解释它。 这些是基准:
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位操作系统和Python。 看起来像我尝试在Intel i7 3.60 GHz上使用Python 3.5.0运行的另一台机器不能重现结果。
使用与timeit.timeit()
相同的Python进程运行@ 10000循环分别产生0.703和0.804。 仍显示尽管程度较轻。 (〜12.5%)
我的结果与您的结果相似:使用中间变量的代码在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
使用-mtimeit
时,我从未设法接近前者的-mtimeit
。
所以这个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
从obmalloc.c
。
obmalloc.c
说
在Python 2.5之前,arenas从来都不是
free()
。 从Python 2.5开始,我们尝试使用free()
竞技场,并使用一些温和的启发式策略来增加最终可以释放竞技场的可能性。
因此,这些启发式以及Python对象分配器在它们清空后立即释放这些免费竞技场的事实导致python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))'
触发病理行为,其中一个256 kiB存储区重复分配和释放; 这种分配发生在mmap
/ munmap
,由于它们是系统调用,因此成本相对较高 - 而且,带有MAP_ANONYMOUS
mmap
要求新映射的页面必须归零 - 即使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
。 因此,在第一次迭代之后,如果不是3,则总是存在至少2个这样的元组,因此不会发生颠簸。
最值得注意的是,不能保证使用中间变量的代码总是更快 - 实际上在某些设置中,使用中间变量可能会导致额外的mmap
调用,而直接比较返回值的代码可能没问题。
当timeit
禁用垃圾收集时,有人问为什么会发生这种情况。 这的确是真实的timeit
做它 :
注意
默认情况下,
timeit()
会在计时期间暂时关闭垃圾回收。 这种方法的优点在于它使独立时序更具可比性。 该缺点是GC可能是所测量功能的重要组成部分。 如果是这样,可以重新启用GC作为设置字符串中的第一个语句。 例如:
然而,Python的垃圾收集器仅用于回收循环垃圾 ,即其引用形成循环的对象的集合。 情况并非如此; 相反,当引用计数降至零时,这些对象立即被释放。
这里的第一个问题必须是,它是否可重复? 对于我们中的一些人来说,至少它肯定是因为其他人说他们没有看到效果。 这在Fedora上使用测试改为平等is
作为实际做的比较似乎风马牛不相及的结果,并推高了20万作为该范围似乎最大限度地发挥作用:
$ python3 -m timeit "a = tuple(range(200000)); b = tuple(range(200000)); a is b"
100 loops, best of 3: 7.03 msec per loop
$ python3 -m timeit "a = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "a = b = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 9.99 msec per loop
$ python3 -m timeit "a = b = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.1 msec per loop
$ python3 -m timeit "a = tuple(range(200000)); b = tuple(range(200000)); a is b"
100 loops, best of 3: 7 msec per loop
$ python3 -m timeit "a = tuple(range(200000)); b = tuple(range(200000)); a is b"
100 loops, best of 3: 7.02 msec per loop
我注意到运行之间的差异以及运行表达式的顺序对结果的影响非常小。
将a
和b
赋值添加到慢速版本中并不会加快速度。 事实上,正如我们可能预期的那样,分配局部变量的效果可以忽略不计。 唯一可以加快速度的是将表达式完全分成两部分。 这应该是唯一的区别是它减少了Python在评估表达式时使用的最大堆栈深度(从4到3)。
这为我们提供了线索,即效果与堆栈深度有关,也许额外的级别会将堆栈推送到另一个内存页面。 如果是这样,我们应该看到影响堆栈的其他更改会发生变化(很可能会导致效果消失),事实上这就是我们所看到的:
$ python3 -m timeit -s "def foo():
tuple(range(200000)) is tuple(range(200000))" "foo()"
100 loops, best of 3: 10 msec per loop
$ python3 -m timeit -s "def foo():
tuple(range(200000)) is tuple(range(200000))" "foo()"
100 loops, best of 3: 10 msec per loop
$ python3 -m timeit -s "def foo():
a = tuple(range(200000)); b = tuple(range(200000)); a is b" "foo()"
100 loops, best of 3: 9.97 msec per loop
$ python3 -m timeit -s "def foo():
a = tuple(range(200000)); b = tuple(range(200000)); a is b" "foo()"
100 loops, best of 3: 10 msec per loop
因此,我认为效果完全是由于在计时过程中消耗了多少Python堆栈。 但它仍然很奇怪。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.