NumPy配列を反復処理すると、NumbaはCythonよりも劇的に高速に見えます。
どのCython最適化が欠けている可能性がありますか?
簡単な例を次に示します。
import numpy as np
def f(arr):
res=np.zeros(len(arr))
for i in range(len(arr)):
res[i]=(arr[i])**2
return res
arr=np.random.Rand(10000)
%timeit f(arr)
出力:ループあたり4.81ms±72.2µs(7回の実行の平均±標準偏差、各100ループ)
%load_ext cython
%%cython
import numpy as np
cimport numpy as np
cimport cython
from libc.math cimport pow
#@cython.boundscheck(False)
#@cython.wraparound(False)
cpdef f(double[:] arr):
cdef np.ndarray[dtype=np.double_t, ndim=1] res
res=np.zeros(len(arr),dtype=np.double)
cdef double[:] res_view=res
cdef int i
for i in range(len(arr)):
res_view[i]=pow(arr[i],2)
return res
arr=np.random.Rand(10000)
%timeit f(arr)
出力:ループあたり445 µs±5.49 µs(7回の実行の平均±標準偏差、各1000ループ)
import numpy as np
import numba as nb
@nb.jit(nb.float64[:](nb.float64[:]))
def f(arr):
res=np.zeros(len(arr))
for i in range(len(arr)):
res[i]=(arr[i])**2
return res
arr=np.random.Rand(10000)
%timeit f(arr)
出力:ループあたり9.59 µs±98.8 ns(7回の実行の平均±標準偏差、各100000ループ)
この例では、NumbaはCythonよりもほぼ50倍高速です。
Cythonの初心者なので、何かが足りないと思います。
もちろん、この単純なケースでは、NumPy square
ベクトル化関数を使用する方がはるかに適していたでしょう。
%timeit np.square(arr)
出力:ループあたり5.75 µs±78.9 ns(7回の実行の平均±標準偏差、各100000ループ)
@Antonioが指摘しているように、単純な乗算にpow
を使用することはあまり賢明ではなく、かなりのオーバーヘッドにつながります。
したがって、pow(arr[i], 2)
を_arr[i]*arr[i]
_に置き換えると、かなり大幅に高速化されます。
_cython-pow-version 356 µs
numba-version 11 µs
cython-mult-version 14 µs
_
残りの違いは、おそらくコンパイラと最適化のレベルの違いによるものです(私の場合はllvmとMSVC)。 numbaのパフォーマンスに合わせてclangを使用することをお勧めします(たとえば、これを参照してください SO-answer )
コンパイラの最適化を容易にするために、入力を連続配列として宣言する必要があります。つまり、_double[::1] arr
_( この質問 ベクトル化にとって重要な理由を参照)、@cython.boundscheck(False)
(オプション_-a
_を使用して、黄色が少ないことを確認します)また、コンパイラーフラグ(つまり、コンパイラーに応じて_-O3
_、_-march=native
_など)を追加して、ベクトル化を有効にします。いくつかの最適化を妨げる可能性のあるデフォルトで使用されるビルドフラグの場合、たとえば -fwrapv )。最後に、working-horse-loopをCで記述し、flags/compilerの正しい組み合わせでコンパイルし、Cythonを使用してラップすることをお勧めします。
ちなみに、関数のパラメーターをnb.float64[:](nb.float64[:])
と入力すると、numbaのパフォーマンスが低下します。入力配列が連続であると想定できなくなり、ベクトル化が除外されます。 numbaにタイプを検出させて(または連続として定義してください。つまり、_nb.float64[::1](nb.float64[::1]
_)、パフォーマンスが向上します。
_@nb.jit(nopython=True)
def nb_vec_f(arr):
res=np.zeros(len(arr))
for i in range(len(arr)):
res[i]=(arr[i])**2
return res
_
次の改善につながります。
_%timeit f(arr) # numba version
# 11.4 µs ± 137 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit nb_vec_f(arr)
# 7.03 µs ± 48.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
_
@ max9111で指摘されているように、結果の配列をゼロで初期化する必要はありませんが、np.empty(...)
の代わりにnp.zeros(...)
を使用できます-このバージョンはnumpyのnp.square()
私のマシンでのさまざまなアプローチのパフォーマンスは次のとおりです。
_numba+vectorization+empty 3µs
np.square 4µs
numba+vectorization 7µs
numba missed vectorization 11µs
cython+mult 14µs
cython+pow 356µs
_