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位操作系统和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时,我从未设法接近前者的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区域,无零件充满了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调用似乎来自Objects / obmalloc.c中的函数_PyObject_ArenaMmap; obmalloc.c还包含宏ARENA_SIZE,其#defined为(256 << 10)(即262144);类似地,munmap与obmalloc.c中的_PyObject_ArenaMunmap匹配. obmalloc.c说
Prior to Python 2.5, arenas were never
free()
‘ed. Starting with Python 2.5,
we do try tofree()
arenas, and use some mild heuristic strategies to increase
the likelihood that arenas eventually can be freed.
因此,这些启发式以及Python对象分配器一旦清空就释放这些免费竞技场的事实导致python3 -mtimeit’元组(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
does it确实如此:
Note
By default,
timeit()
temporarily turns off garbage collection during the timing. The advantage of this approach is that it makes independent timings more comparable. This disadvantage is that GC may be an important component of the performance of the function being measured. If so, GC can be re-enabled as the first statement in the setup string. For example:
然而,Python的垃圾收集器仅用于回收循环垃圾,即其引用形成循环的对象的集合.情况并非如此;相反,当引用计数降至零时,这些对象立即被释放.
标签:cpython,python,python-3-x,python-internals 来源: https://codeday.me/bug/20191003/1850545.html