私は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
観察されたパフォーマンスの違いに(関連して)寄与するいくつかの要因があります。
Zip
は、次の__next__
呼び出しが行われたときに、参照カウントが1の場合、返されたTuple
を再利用します。map
はnewTuple
を構築し、__next__
呼び出しが行われるたびに「マップされた関数」に渡されます。 Pythonは未使用のタプルのストレージを維持するため、実際には新しいタプルを最初から作成することはおそらくありません。ただし、その場合、map
は適切なサイズの未使用のタプルを見つける必要があります。 。starmap
は、反復可能オブジェクトの次の項目がTuple
型であるかどうかをチェックし、そうである場合は、それを渡すだけです。PyObject_Call
を使用してCコード内からC関数を呼び出しても、呼び出し先に渡される新しいタプルは作成されません。したがって、starmap
とZip
は、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
したがって、タプルは直接渡されます(これらが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
を呼び出すよりも、Zip
とmap
の方が高速であるという点に至るまでの多くの「一致」があります。 ...。
私が気付くことができる1つの違いは、map
がイテラブルからアイテムを取得する方法です。 map
と Zip
の両方が、渡された各イテレータからイテレータのタプルを作成します。これで、Zip
は result 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