地图与星图的表现?

god*_*ygo 14 python performance cpython python-itertools python-3.x

我试图制作一个纯python(没有外部依赖)两个序列的元素比较.我的第一个解决方案是

list(map(operator.eq, seq1, seq2))
Run Code Online (Sandbox Code Playgroud)

然后我发现了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)
Run Code Online (Sandbox Code Playgroud)

在检索元素时,第二种解决方案的性能提高了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)
Run Code Online (Sandbox Code Playgroud)

我不知道如何深入挖掘这种嵌套代码的分析?所以我的问题是为什么第一台发电机的检索速度如此之快以及我们在哪里获得额外13%的list功能?

编辑:我的第一个目的是执行元素比较而不是all,所以all函数被替换为list.此替换不会影响时序比率.

Windows 10(64位)上的CPython 3.6.2

MSe*_*ert 7

有几个因素(与此相关)对观察到的性能差异有贡献:

  • ziptuple如果在下一次__next__调用时引用计数为1,则重新使用返回的值.
  • map构建一个新的 tuple,每次__next__调用时传递给"映射函数" .实际上它可能不会从头开始创建一个新的元组,因为Python为未使用的元组维护了一个存储空间.但在这种情况下map,必须找到一个大小合适的未使用的元组.
  • starmap检查迭代中的下一个项是否为类型tuple,如果是,则将其传递给它.
  • 从C代码中调用C函数PyObject_Call不会创建传递给被调用者的新元组.

因此starmap,zip只会一次又一次地使用一个元组传递给它,operator.eq从而极大地减少了函数调用开销.map另一方面,每次operator.eq调用时都会创建一个新的元组(或从CPython 3.6中填充一个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)
Run Code Online (Sandbox Code Playgroud)

是的,tuples并不是真正的一成不变的,一个简单Py_DECREF就足以"欺骗" zip相信没有人能够引用返回的元组!

至于"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
Run Code Online (Sandbox Code Playgroud)

所以元组是直接传递的(因为它们被定义为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
Run Code Online (Sandbox Code Playgroud)

请注意,即使从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
Run Code Online (Sandbox Code Playgroud)

所以有很多的"巧合"领导到该点starmapzip比调用快map多参数时被调用函数也是C函数...