2017-09-12 37 views
6

我试图做一个纯python(没有外部依赖)元素明智的比较两个序列。我的第一个解决方案是:地图vs星图的性能?

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

+1

为什么不直接使用'SEQ1 = = seq2'? –

+0

@Błotosmętek谢谢你的纠正!我的第一个意图是元素比较,而不是'all',这在我的问题中并不明显:)真的,如果你用'list'而不是'all'来代替'all',那么次数的顺序将是相同的。 – godaygo

+0

什么是Python版本?这是CPython吗? – MSeifert

回答

2

有有助于(结合)的几个因素所观察到的性能差异:

  • zip重新使用返回的tuple,如果它具有1的引用计数时下一__next__呼叫被制成。
  • map构建传递给每一个__next__调用时的“映射功能”一tuple。实际上,它可能不会从头创建一个新的元组,因为Python为未使用的元组维护一个存储。但在这种情况下,map必须找到合适大小的未使用的元组。
  • starmap检查迭代器中的下一项是否为tuple类型,如果是,则只传递它。
  • 使用PyObject_Call从C代码中调用C函数不会创建传递给被调用者的新元组。

所以starmapzip将只使用一个元组一遍遍传递给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) 

是,tuple s为不是真的一成不变的,一个简单的Py_DECREF足以“把“zip变成相信没有人拥有对返回元组的引用!

至于“元组直通”:

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 

所以有很多的“巧合”领导到如此地步,starmapzip比调用map有多个参数时快被调用函数也是C函数...

1

一个区别我可以看到的是如何map检索来自iterables项目。 mapzip都从每个可传递的迭代中创建一个迭代器的元组。现在zip在内部维护一个result tuple,每当下一次被调用时填充,而在另​​一方面,每次下一次调用时填充result tuple,并释放它。


* 正如指出的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

+0

这会假设这是3.6,请注意[3.5.4]中的代码(https://github.com/python/cpython/blob/v3 .5.4/Python/bltinmodule.c#L1168-L1192)看起来不同。 :) – MSeifert

+0

@ MSeifert我不知道有多少缓慢/快3.5.4实施与3.6.2相比。 –

+0

(由于堆与C-Stack的访问,应该慢到5次迭代) –