Bha*_*rel 76 python cpython python-3.x python-internals
我遇到了这种奇怪的行为并且无法解释它.这些是基准:
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
Run Code Online (Sandbox Code Playgroud)
为什么与变量赋值的比较比使用临时变量的单个衬里快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()
@ 10000循环相同的Python进程运行分别产生0.703和0.804.仍显示尽管程度较轻.(〜12.5%)
Ant*_*ala 106
我的结果与你的结果相似:使用中间变量的代码在我厌烦的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
Run Code Online (Sandbox Code Playgroud)
值得注意的是,当我-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
Run Code Online (Sandbox Code Playgroud)
现在这是差异的一个很好的理由.不使用变量的代码会导致mmap
系统调用被调用的几乎比使用中间变量的调用多1000倍.
所述withoutvars
充满mmap
/ munmap
用于256K区域; 这些相同的行一遍又一遍地重复:
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
Run Code Online (Sandbox Code Playgroud)
的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
,因为他们是系统调用,这是比较昂贵的-而且,mmap
与MAP_ANONYMOUS
要求新映射的页面必须清零-尽管Python的也不会在意.
该行为是不存在在使用中间变量,因为它是使用略微代码更多存储器和一些对象在它仍然没有分配存储器竞技场可以被释放.那是因为timeit
将它变成一个不同的循环
for n in range(10000)
a = tuple(range(2000))
b = tuple(range(2000))
a == b
Run Code Online (Sandbox Code Playgroud)
现在的行为是两个a
并且b
将一直保持绑定直到它们被重新分配,所以在第二次迭代中,tuple(range(2000))
将分配第三个元组,并且赋值a = tuple(...)
将减少旧元组的引用计数,导致它被释放,并且增加新元组的引用计数; 然后同样的事情发生了b
.因此,在第一次迭代之后,如果不是3,则总是存在至少2个这样的元组,因此不会发生颠簸.
最值得注意的是,无法保证使用中间变量的代码总是更快 - 实际上在某些设置中,使用中间变量可能会导致额外的mmap
调用,而直接比较返回值的代码可能会很好.
有人问为什么会发生这种情况,当timeit
禁用垃圾收集时.这的确是事实,timeit
做它:
注意
默认情况下,
timeit()
在计时期间暂时关闭垃圾回收.这种方法的优点在于它使独立时序更具可比性.该缺点是GC可能是所测量功能的重要组成部分.如果是这样,可以重新启用GC作为设置字符串中的第一个语句.例如:
然而,Python的垃圾收集器仅用于回收循环垃圾,即其引用形成循环的对象的集合.情况并非如此; 相反,当引用计数降至零时,这些对象立即被释放.
这里的第一个问题必须是,它是否可重复?对于我们中的一些人来说,至少它肯定是因为其他人说他们没有看到效果.在Fedora上,将相等测试更改is
为实际进行比较似乎与结果无关,并且范围推高至200,000,因为这似乎最大化效果:
$ 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
Run Code Online (Sandbox Code Playgroud)
我注意到运行之间的差异以及运行表达式的顺序对结果的影响非常小.
向慢速版本添加作业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
Run Code Online (Sandbox Code Playgroud)
因此,我认为效果完全是由于在计时过程中消耗了多少Python堆栈.但它仍然很奇怪.