web-dev-qa-db-ja.com

NumPy配列を反復処理するときに、CythonがNumbaよりもはるかに遅いのはなぜですか?

NumPy配列を反復処理すると、NumbaはCythonよりも劇的に高速に見えます。
どのCython最適化が欠けている可能性がありますか?

簡単な例を次に示します。

純粋なPythonコード:

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ループ)


Cythonコード(Jupyter内):

%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ループ)


Numbaコード:

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ループ)

10
Greg A

@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
_
10
ead