2017-09-12 12 views
6

私は、外部依存関係のない純粋なPythonを2つのシーケンスの要素ごとに比較しようとしていました。私の最初の解決策だった:マップのパフォーマンスとstarmapのパフォーマンス?

list(map(operator.eq, seq1, seq2)) 

それから私は、私にはかなり似て見えたこれ、itertoolsからstarmap機能を発見しました。しかし、私のコンピュータで最悪の場合、それは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) 

第2の解決策は、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%の余分なゲインを得る理由です。

EDIT: all機能がlistと交換したように、私の最初の意図は、代わりにallの要素単​​位の比較を実行することでした。この置き換えはタイミング比に影響しません。私は気づくことができる

のWindows 10上ではCPython 3.6.2(64)

+1

が理由だけではなく、使用しないで...と呼ばれる機能もCの関数であるときzipstarmapが複数の引数を持つmapを呼び出すよりも高速である点に至るまでの「偶然の一致」が多い 'SEQ1 = = seq2'? –

+0

@Błotosmętek訂正ありがとう!私の最初の意図は 'all'ではなく、要素的な比較でした。これは私の質問からは分かりませんでした:)本当に' all'の代わりに 'list'を置き換えると、時間の順序は同じになります。 – godaygo

+0

Pythonのバージョンは?そして、これはCPythonですか? – MSeifert

答えて

2

観察の性能差に(一緒に)貢献するいくつかの要因があります。

  • zip再利用し、それは1の参照カウントを持っている場合に返さtupleを次の__next__呼び出しは作成されます。
  • mapは、「マッピングされた機能」__next__呼び出しが行われるたびに渡され新しいtupleを構築します。実際には、Pythonは未使用のタプルのためのストレージを保持しているので、新しいタプルを最初から作成することはないでしょう。しかしその場合、mapは正しいサイズの未使用タプルを見つけなければなりません。
  • starmapは、イテラブル内の次のアイテムがタイプtupleであるかどうかをチェックし、そうであればそれを渡します。
  • PyObject_CallでCコード内からC関数を呼び出すと、呼び出し先に渡される新しいタプルは作成されません。

のでzipstarmapはそれだけでは、このように非常関数呼び出しのオーバーヘッドを削減operator.eqに渡され、何度も何度も1つのタプルを使用します。一方、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 

だから、タプルが右を通過する。これは、純粋なPythonの関数のために発生しません(これらは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関数から呼び出された場合でも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 

だから

1

1つの違いはmapが反復可能オブジェクトからアイテムを取得する方法です。 mapzipの両方は、渡された各反復子からの反復子のタプルを作成します。今度はzipは、result tupleを内部的に維持します。これは、nextが呼び出されるたびに入力され、一方、map creates a new array*は次の呼び出しごとに割り当てられ、割り当てが解除されます。


* としては、新しいPythonのタプル毎回を割り当てるために使用3.5.4 map_nextまでMSeifertによって指摘しました。これは3.6で変更され、5 iterablesまでCスタックが使用され、それ以上のものについてはヒープが使用されます。関連PR:Issue #27809: map_next() uses fast callおよびAdd _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回のイテラブルまで遅くなるはずです) –

関連する問題