以下の例を考えてみましょう。コアが 2 つの入力配列とループ インデックスの関数を含む数値計算を行う 2 レベルのネストされた for ループが 4 つの異なる方法で実行されます。各バリアントは、Ipython の%timeit
魔法でタイミングがとられています。
- numba.jit を使用してコンパイルされた単純な for ループ
- シングルスレッドで実行される、numba.guvectorize を使用した forall のような構造 (
target = "cpu"
)
- numba.guvectorize を使用した forall のような構造で、CPU の「コア」 (私の場合はハイパースレッド) と同じ数のスレッドで実行されます (
target = "parallel"
)
- 3. と同じですが、ランダムに並べ替えられた「並列」ループ インデックスのシーケンスで「guvectorized」forall を呼び出します。
(この特定の例では) 内側のループの範囲が外側のループのインデックスの値に依存するため、最後の 1 つが実行されます。gufunc 呼び出しのディスパッチが numpy 内でどのように編成されているかは正確にはわかりませんが、「並列」ループ インデックスのランダム化により負荷分散がわずかに向上するようです。
私の(遅い)マシン(第1世代コアi5、2コア、4ハイパースレッド)では、タイミングが得られます:
1 loop, best of 3: 8.19 s per loop
1 loop, best of 3: 8.27 s per loop
1 loop, best of 3: 4.6 s per loop
1 loop, best of 3: 3.46 s per loop
注: このレシピが target="gpu" にすぐに適用されるかどうか (適用されるはずですが、現在適切なグラフィックス カードにアクセスできません)、高速化はどうなのか興味があります。投稿してください!
そして、ここに例があります:
import numpy as np
from numba import jit, guvectorize, float64, int64
@jit
def naive_for_loop(some_input_array, another_input_array, result):
for i in range(result.shape[0]):
for k in range(some_input_array.shape[0] - i):
result[i] += some_input_array[k+i] * another_input_array[k] * np.sin(0.001 * (k+i))
@guvectorize([(float64[:],float64[:],int64[:],float64[:])],'(n),(n),()->()', nopython=True, target='parallel')
def forall_loop_body_parallel(some_input_array, another_input_array, loop_index, result):
i = loop_index[0] # just a shorthand
# do some nontrivial calculation involving elements from the input arrays and the loop index
for k in range(some_input_array.shape[0] - i):
result[0] += some_input_array[k+i] * another_input_array[k] * np.sin(0.001 * (k+i))
@guvectorize([(float64[:],float64[:],int64[:],float64[:])],'(n),(n),()->()', nopython=True, target='cpu')
def forall_loop_body_cpu(some_input_array, another_input_array, loop_index, result):
i = loop_index[0] # just a shorthand
# do some nontrivial calculation involving elements from the input arrays and the loop index
for k in range(some_input_array.shape[0] - i):
result[0] += some_input_array[k+i] * another_input_array[k] * np.sin(0.001 * (k+i))
arg_size = 20000
input_array_1 = np.random.rand(arg_size)
input_array_2 = np.random.rand(arg_size)
result_array = np.zeros_like(input_array_1)
# do single-threaded naive nested for loop
# reset result_array inside %timeit call
%timeit -r 3 result_array[:] = 0.0; naive_for_loop(input_array_1, input_array_2, result_array)
result_1 = result_array.copy()
# do single-threaded forall loop (loop indices in-order)
# reset result_array inside %timeit call
loop_indices = range(arg_size)
%timeit -r 3 result_array[:] = 0.0; forall_loop_body_cpu(input_array_1, input_array_2, loop_indices, result_array)
result_2 = result_array.copy()
# do multi-threaded forall loop (loop indices in-order)
# reset result_array inside %timeit call
loop_indices = range(arg_size)
%timeit -r 3 result_array[:] = 0.0; forall_loop_body_parallel(input_array_1, input_array_2, loop_indices, result_array)
result_3 = result_array.copy()
# do forall loop (loop indices scrambled for better load balancing)
# reset result_array inside %timeit call
loop_indices_scrambled = np.random.permutation(range(arg_size))
loop_indices_unscrambled = np.argsort(loop_indices_scrambled)
%timeit -r 3 result_array[:] = 0.0; forall_loop_body_parallel(input_array_1, input_array_2, loop_indices_scrambled, result_array)
result_4 = result_array[loop_indices_unscrambled].copy()
# check validity
print(np.all(result_1 == result_2))
print(np.all(result_1 == result_3))
print(np.all(result_1 == result_4))