numexpr、numba、およびcythonがあります。この回答の目的は、これらの可能性を考慮することです。
しかし、最初に明白なことを述べましょう。Python関数をnumpy配列にどのようにマップしても、それはPython関数のままであり、すべての評価に対して意味があります。
- numpy-array要素はPythonオブジェクト(例:
Float
。
- すべての計算はPythonオブジェクトで行われます。つまり、インタープリター、動的ディスパッチ、および不変オブジェクトのオーバーヘッドが発生します。
そのため、実際に配列をループするために使用される機構は、上記のオーバーヘッドのため、大きな役割を果たすことはありません。numpyの組み込み機能を使用するよりもずっと遅いままです。
次の例を見てみましょう。
# numpy-functionality
def f(x):
return x+2*x*x+4*x*x*x
# python-function as ufunc
import numpy as np
vf=np.vectorize(f)
vf.__name__="vf"
np.vectorize
pure-python関数クラスのアプローチの代表として選択されています。を使用してperfplot
(この回答の付録のコードを参照)、次の実行時間を取得します。
numpy-approachは、純粋なpythonバージョンよりも10倍から100倍速いことがわかります。配列サイズが大きくなるとパフォーマンスが低下するのは、おそらくデータがキャッシュに収まらないためです。
これも言及する価値vectorize
があります。これも多くのメモリを使用するため、メモリ使用量がボトルネックになることがよくあります(関連するSO質問を参照)。また、そのnumpyのドキュメントnp.vectorize
は、「パフォーマンスではなく、主に利便性のために提供されている」と記載されています。
他のツールを使用する必要があります。パフォーマンスが必要な場合は、C拡張を最初から作成する以外に、次の可能性があります。
それはボンネットの下で純粋なCであるので、派手なパフォーマンスはそれが得られるのと同じくらい良いとよく聞かれます。しかし、改善の余地はたくさんあります!
ベクトル化されたnumpy-versionは、多くの追加メモリとメモリアクセスを使用します。numexp-libraryはnumpy-arrayを並べて表示し、キャッシュの使用率を向上させます。
# less cache misses than numpy-functionality
import numexpr as ne
def ne_f(x):
return ne.evaluate("x+2*x*x+4*x*x*x")
次の比較につながります。
上記のプロットですべてを説明することはできません。最初にnumexpr-libraryのオーバーヘッドが大きくなっているのがわかりますが、キャッシュの利用効率が高いため、配列が大きいほど約10倍速くなります。
別のアプローチは、関数をjitコンパイルして、実際の純粋なC UFuncを取得することです。これはnumbaのアプローチです:
# runtime generated C-function as ufunc
import numba as nb
@nb.vectorize(target="cpu")
def nb_vf(x):
return x+2*x*x+4*x*x*x
元のnumpyアプローチよりも10倍高速です。
ただし、タスクは非常に並列化可能prange
であるため、ループを並列で計算するために使用することもできます。
@nb.njit(parallel=True)
def nb_par_jitf(x):
y=np.empty(x.shape)
for i in nb.prange(len(x)):
y[i]=x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y
予想どおり、並列関数は入力が小さい場合は遅くなりますが、サイズが大きい場合は速くなります(ほぼ2)。
numbaはnumpy配列を使用した操作の最適化を専門としていますが、Cythonはより一般的なツールです。numbaと同じパフォーマンスを抽出するのはより複雑です-多くの場合、llvm(numba)とローカルコンパイラ(gcc / MSVC)の違いになります。
%%cython -c=/openmp -a
import numpy as np
import cython
#single core:
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_f(double[::1] x):
y_out=np.empty(len(x))
cdef Py_ssize_t i
cdef double[::1] y=y_out
for i in range(len(x)):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
#parallel:
from cython.parallel import prange
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_par_f(double[::1] x):
y_out=np.empty(len(x))
cdef double[::1] y=y_out
cdef Py_ssize_t i
cdef Py_ssize_t n = len(x)
for i in prange(n, nogil=True):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
Cythonの結果、機能が多少遅くなります。
結論
明らかに、1つの関数のみをテストしても何も証明されません。また、選択した関数の例では、メモリの帯域幅が10 ^ 5要素より大きいサイズのボトルネックであったことにも注意してください。この領域では、numba、numexpr、およびcythonで同じパフォーマンスが得られました。
結局のところ、最終的な答えは、機能の種類、ハードウェア、Pythonの配布、その他の要因によって異なります。たとえば、Anacondaディストリビューションは、numpyの関数にIntelのVMLを使用しているため、numbaよりも優れています(SVMLを使用しない限り、これを参照してください) SO-ポストを)簡単のような超越関数のためにexp
、sin
、cos
および類似-例えば以下を参照してくださいSO-ポストを。
しかし、この調査とこれまでの私の経験から、超越関数が含まれていない限り、numbaは最高のパフォーマンスを発揮する最も簡単なツールであるように思われます。
perfplot -package を使用して実行時間をプロットする:
import perfplot
perfplot.show(
setup=lambda n: np.random.rand(n),
n_range=[2**k for k in range(0,24)],
kernels=[
f,
vf,
ne_f,
nb_vf, nb_par_jitf,
cy_f, cy_par_f,
],
logx=True,
logy=True,
xlabel='len(x)'
)