variables分配比一个class轮更快
我遇到了这个奇怪的行为,但没有解释。 这些是基准:
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
与variables赋值相比,如何比使用临时variables的单线程快27%以上?
通过Python文档,垃圾收集在timeit期间被禁用,所以不能这样做。 这是一种优化?
结果也可能在Python 2.x中被重现,尽pipe程度较低。
运行Windows 7,CPython 3.5.1,Intel i7 3.40 GHz,64位操作系统和Python。 看起来像一个不同的机器,我已经尝试运行在英特尔i7 3.60 GHz与Python 3.5.0不会重现结果。
使用timeit.timeit()
@ 10000循环使用相同的Python进程运行分别产生0.703和0.804。 尽pipe程度还是有所下降的。 (〜12.5%)
我的结果与您的结果相似:使用variables的代码相当一致地快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
。
所以我决定用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
现在这是一个很好的理由。 不使用variables的代码会导致mmap
系统调用比使用中间variables的调用多1000倍。
对于一个256k的区域,这个withoutvars
是满的。 这些相同的路线一遍又一遍地重复着:
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
调用似乎来自Objects/obmalloc.c
的函数Objects/obmalloc.c
; obmalloc.c
也包含macrosARENA_SIZE
,它是#define
d是(256 << 10)
(即262144
); munmap
与_PyObject_ArenaMunmap
中的_PyObject_ArenaMunmap
匹配。
obmalloc.c
说
在Python 2.5之前,竞技场从来不是
free()
。 从Python 2.5开始,我们尝试free()
竞技场,并使用一些温和的启发式策略来增加竞技场最终可以被释放的可能性。
因此,这些启发式以及Python对象分配器在清空后立即释放这些空闲领域的事实导致python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))'
触发病态行为,其中256 kiB内存区被重新分配和重复释放; 而且这个分配发生在mmap
/ munmap
,这个系统调用的代价相对昂贵 – 而且, MAP_ANONYMOUS
mmap
要求新映射的页面必须清零 – 即使Python不在意。
该行为似乎并没有与使用中间variables的代码一起出现,可能是因为它使用了更多的内存,从来没有从上一个内存区域释放所有对象。 最值得注意的是,不能保证使用中间variables的代码总是更快 – 实际上,在某些设置中,使用中间variables会导致额外的mmap
调用,而直接比较返回值的代码可能没问题。
这里的第一个问题是,是否可重现? 至less对我们中的一些人来说,虽然其他人说他们没有看到效果。 在Fedora上,平等testing变成实际上做的比较似乎与结果无关,而范围被推高到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
我注意到运行之间的差异以及运行expression式的顺序对结果几乎没有影响。
将a
和b
分配添加到慢速版本不会加快速度。 事实上,我们可以预计分配给局部variables的影响可以忽略不计。 唯一能加快速度的是将expression式完全分成两部分。 这个唯一的区别就是在评估expression式(从4到3)的时候减less了Python使用的最大堆栈深度。
这给了我们线索效应与堆栈深度有关,也许额外的水平推动堆栈到另一个内存页面。 如果是这样,我们应该看到,影响堆栈的其他更改将会改变(最可能的是杀死这个效果),实际上我们看到:
$ 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
所以,我认为效果完全是由于在计时过程中消耗了多lessPython堆栈。 尽pipe如此,这还是很奇怪的。