web-dev-qa-db-ja.com

マップとスターマップのパフォーマンス?

私は2つのシーケンスの純粋な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)

要素を取得する場合、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%余分に得られるのですか?

編集:私の最初の意図は、allの代わりに要素ごとの比較を実行することでした。そのため、all関数はlistに置き換えられました。この交換はタイミング比には影響しません。

Windows10(64ビット)上のCPython 3.6.2

15
godaygo

観察されたパフォーマンスの違いに(関連して)寄与するいくつかの要因があります。

  • Zipは、次の__next__呼び出しが行われたときに、参照カウントが1の場合、返されたTupleを再利用します。
  • mapnewTupleを構築し、__next__呼び出しが行われるたびに「マップされた関数」に渡されます。 Pythonは未使用のタプルのストレージを維持するため、実際には新しいタプルを最初から作成することはおそらくありません。ただし、その場合、mapは適切なサイズの未使用のタプルを見つける必要があります。 。
  • starmapは、反復可能オブジェクトの次の項目がTuple型であるかどうかをチェックし、そうである場合は、それを渡すだけです。
  • PyObject_Callを使用してCコード内からC関数を呼び出しても、呼び出し先に渡される新しいタプルは作成されません。

したがって、starmapZipは、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)

はい、Tuplesは実際には不変ではありません。単純な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

したがって、呼び出された関数がC関数でもある場合、複数の引数を使用してstarmapを呼び出すよりも、Zipmapの方が高速であるという点に至るまでの多くの「一致」があります。 ...。

7
MSeifert

私が気付くことができる1つの違いは、mapがイテラブルからアイテムを取得する方法です。 mapZip の両方が、渡された各イテレータからイテレータのタプルを作成します。これで、Zipresult Tuple を内部的に維持し、次に呼び出されるたびに入力されますが、 mapは新しい配列を作成します* 次の呼び出しごとに、割り当てを解除します。


*MSeifertが3.5.4まで指摘したようにmap_next新しいPythonタプルを毎回割り当てるために使用されます。これは3.6で変更され、5つの反復可能Cスタックが使用され、そのヒープよりも大きいものが使用されます。関連PR: 問題#27809:map_next()は高速呼び出しを使用します および _ PY_FASTCALL_SMALL_STACK定数を追加 |問題: https://bugs.python.org/issue27809

1