我试图对两个序列进行纯Python(没有外部依赖项)元素方面的比较。我的第一个解决方案是:
list(map(operator.eq, seq1, seq2))
然后我发现
starmap
函数来自itertools
,它看起来和我很相似。但在最坏的情况下,它在我的电脑上比以前快了37%。由于这对我来说并不明显,我测量了从生成器中检索1个元素所需的时间(不知道这种方法是否正确):from operator import eq
from itertools import starmap
seq1 = [1,2,3]*10000
seq2 = [1,2,3]*10000
seq2[-1] = 5
gen1 = map(eq, seq1, seq2))
gen2 = starmap(eq, zip(seq1, seq2))
%timeit -n1000 -r10 next(gen1)
%timeit -n1000 -r10 next(gen2)
271 ns ± 1.26 ns per loop (mean ± std. dev. of 10 runs, 1000 loops each)
208 ns ± 1.72 ns per loop (mean ± std. dev. of 10 runs, 1000 loops each)
在检索元素时,第二个解决方案的性能提高了24%。在那之后,它们都为
list
产生了相同的结果。但从某个地方我们可以获得额外13%的时间:%timeit list(map(eq, seq1, seq2))
%timeit list(starmap(eq, zip(seq1, seq2)))
5.24 ms ± 29.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.34 ms ± 84.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
我不知道如何更深入地分析这种嵌套代码?所以我的问题是,为什么第一个生成器在检索方面速度如此之快,从中我们在
list
函数中获得额外的13%?编辑:
我的第一个目的是执行元素比较而不是
all
,因此将all
函数替换为list
。这种更换不会影响正时比。cpython 3.6.2在Windows 10上(64位)
最佳答案
导致观察到的性能差异的因素有几个:
如果下次调用时引用计数为1,则重新使用返回的。zip
生成一个新的tuple
每次调用时都传递给“mapped function”。实际上,它可能不会从头开始创建新的元组,因为Python为未使用的元组维护存储。但是在这种情况下,__next__
必须找到一个大小合适的未使用的元组。map
检查iterable中的下一个项是否属于类型tuple
,如果是,则它只是传递它。
使用__next__
从C代码中调用C函数不会创建传递给被调用方的新元组。
因此,map
withstarmap
将只使用一个反复传递给tuple
的元组,从而大大减少函数调用开销。另一方面,每次调用PyObject_Call
时,都会创建一个新的元组(或从cpython 3.6 on填充一个c数组)。所以,实际的速度差只是元组的创建开销。
我将提供一些可以用来验证这一点的cython代码,而不是链接到源代码:
In [1]: %load_ext cython
In [2]: %%cython
...:
...: from cpython.ref cimport Py_DECREF
...:
...: cpdef func(zipper):
...: a = next(zipper)
...: print('a', a)
...: Py_DECREF(a)
...: b = next(zipper)
...: print('a', a)
In [3]: func(zip([1, 2], [1, 2]))
a (1, 1)
a (2, 2)
是的,
starmap
s不是真正不变的,一个简单的zip
就足以“欺骗”operator.eq
相信没有其他人持有对返回的元组的引用!关于“tuple pass-thru”的内容:
In [4]: %%cython
...:
...: def func_inner(*args):
...: print(id(args))
...:
...: def func(*args):
...: print(id(args))
...: func_inner(*args)
In [5]: func(1, 2)
1404350461320
1404350461320
所以元组是直接传递的(只是因为这些定义为C函数!)纯Python函数不会发生这种情况:
In [6]: def func_inner(*args):
...: print(id(args))
...:
...: def func(*args):
...: print(id(args))
...: func_inner(*args)
...:
In [7]: func(1, 2)
1404350436488
1404352833800
注意,如果被调用函数不是C函数,即使从C函数调用,也不会发生这种情况:
In [8]: %%cython
...:
...: def func_inner_c(*args):
...: print(id(args))
...:
...: def func(inner, *args):
...: print(id(args))
...: inner(*args)
...:
In [9]: def func_inner_py(*args):
...: print(id(args))
...:
...:
In [10]: func(func_inner_py, 1, 2)
1404350471944
1404353010184
In [11]: func(func_inner_c, 1, 2)
1404344354824
1404344354824
因此,当被调用函数也是一个C函数时,
map
和operator.eq
比用多个参数调用tuple
要快很多“巧合”。