地图与星图的表现?

时间:2017-09-12 08:53:41

标签: python performance python-3.x itertools cpython

我试图制作一个纯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。这种替换不会影响时序比率。

Windows 10(64位)上的CPython 3.6.2

2 个答案:

答案 0 :(得分:6)

有几个因素会导致观察到的性能差异(与此相关):

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

所以带starmap的{​​{1}}只会反复使用一个元组,传递给zip,从而极大地减少了函数调用开销。另一方面,operator.eq将在每次调用map时创建一个新元组(或从CPython 3.6中填充C数组)。那么实际上速度差异只是元组创建开销。

不是链接到源代码,而是提供一些可用于验证的Cython代码:

operator.eq

是的,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) 不是真正不可变的,简单的tuple就足以“哄骗”#34; Py_DECREF相信别人没有提到返回的元组!

至于" tuple-pass-thru":

zip

所以元组是直接传递的(因为它们被定义为C函数!)这对纯Python函数来说不会发生:

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函数调用,被调用函数不是C函数也不会发生:

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函数时,导致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 starmapzip比调用map更快...

答案 1 :(得分:1)

我可以注意到的一个区别是map如何从迭代中检索项目。 mapzip都会从每个传递的迭代中创建一个迭代器元组。现在zip在内部维护一个result tuple,每次调用next时都会填充,另一方面,map creates a new array*每次调用并释放它。

* 正如MSeifert所指出的那样,直到3.5.4 map_next每次都会分配一个新的Python元组。这在3.6和5次迭代中发生了变化,使用了C堆栈,并且使用了大于该堆的任何东西。相关PR:Issue #27809: map_next() uses fast callAdd _PY_FASTCALL_SMALL_STACK constant |问题:https://bugs.python.org/issue27809