我试图对两个序列进行纯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函数不会创建传递给被调用方的新元组。
因此,mapwithstarmap将只使用一个反复传递给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)

是的,starmaps不是真正不变的,一个简单的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函数时,mapoperator.eq比用多个参数调用tuple要快很多“巧合”。

10-06 05:22
查看更多