【Python】総和計算は、自作が速い!?
基本的な処理でもある総和計算、Pythonだと組み込み関数やNumPyの関数などいろいろ関数で求めることができます。
結局、どれが速いのか?どれを使えばいいのか?知らなかったので、 外出自粛期間の暇つぶしに、比較してみました。
総和計算とは
- 複数の入力値の合計を求める処理
- 例
- 入力: 一日の出費(1週間) → 総和計算の結果:一週間の出費
月 | 火 | 水 | 木 | 金 | 土 | 日 |
---|---|---|---|---|---|---|
¥1,200 | ¥300 | ¥2,500 | ¥800 | ¥3,000 | ¥0 | ¥5,000 |
総和結果(一週間の出費) |
---|
¥12,800 |
既存関数の処理速度比較
単純なforループの実装を基準に、Pythonの組み込み関数sum
と、
数値計算のための拡張モジュールNumpPyのsum
関数の速度を評価します。
基準:forループによる総和計算
sum_val = 0 # 総和結果を格納する変数 for item in input_arr: # input_arrは複数の入力を格納している配列 sum_val += item
組み込み関数sum
による総和計算
# sum_valに総和結果が格納される sum_val = sum(input_arr)
NumPyのsum
関数による総和計算
import numpy as np # sum_valに総和結果が格納される sum_val = np.sum(input_arr)
計測プログラム
import time import numpy as np # Generate an input array N = 1024* 1024 * 256 # The number of elements input_arr = np.random.rand(N) print('Init : done (# of elements : ', N, ' )\n', flush=True) print('###### Simple loop summation ######') st = time.time() sum_val = 0 for item in input_arr: sum_val += item elapsed_time = time.time() - st print('\tSummation result\t: ', sum_val) print('\tElapsed time[sec]\t: ', elapsed_time) print('###### Default sum function ######') st = time.time() sum_val = sum(input_arr) elapsed_time = time.time() - st print('\tSummation result\t: ', sum_val) print('\tElapsed time[sec]\t: ', elapsed_time) print('###### NumPy sum function ######') st = time.time() sum_val = np.sum(input_arr) elapsed_time = time.time() - st print('\tSummation result\t: ', sum_val) print('\tElapsed time[sec]\t: ', elapsed_time)
処理時間比較
- 計測環境
- 計測条件
- 入力要素数:256M(=256 * 1024 * 1024)
- 入力配列:NumPy配列
関数 | 処理時間[sec] | 高速化率(基準の何倍速いか) |
---|---|---|
基準:forループ実装 | 143.377 | - |
組み込み関数sum |
64.546 | 2.22 |
NumPyのsum 関数 |
0.144 | 995.67 |
結局、簡単に思いつく既存関数だとNumPyが爆速!
基準としたPythonのforループ実装の約1000倍速い!
とりあえず、総和計算が出てきたらNumPyのsum
関数に渡せば問題ないということがわかりました!
NumPyの大半が、高速処理のためにC言語ベースで開発されているから当たり前の結果か、、
NumPyの総和計算速度に追いつく
forループの実装をベースに、自作関数でNumPyの総和計算速度に追いつくために頑張ります。
まずは、Pythonとの別れ
Pythonで実装していては、C言語で書かれた関数と同程度の速度を出すことは困難です。
そのため、forループ実装の部分を関数として切り出し、 JITコンパイルが可能であるNumbaを使って高速に処理ができる機械語に変換します。
1 . 関数として書き直す
def summation(input_arr): sum_val = 0 for item in input_arr: sum_val += item return sum_val
2 . Numbaの適用
import numba @numba.njit def summation(input_arr): sum_val = 0 for item in input_arr: sum_val += item return sum_val
NumPyとNumbaの比較
一回目の評価結果に追記する形式で行う。
- 計測環境
- 計測条件
- 入力要素数:256M(=256 * 1024 * 1024)
- 入力配列:NumPy配列
関数 | 処理時間[sec] | 高速化率(基準の何倍速いか) |
---|---|---|
基準:forループ実装 | 143.377 | - |
組み込み関数sum |
64.546 | 2.22 |
NumPyのsum 関数 |
0.144 | 995.67 |
forループ実装+Numba | 0.354 | 405.01 |
Numbaを使うだけで、NumPyのsum
関数の40%ぐらいの性能になりました!
あと、2.5倍ぐらいするとNumPyのsum
関数に勝てます!
やはり、速度を出すためにはコンパイルすることは重要ですね。
処理の並列化(マルチコアの活用)
現在のCPUは、内部に複数の演算器(コア)を持っています。
Pythonとかで単純にコードを書いて実行すると、 1コアしか使いません。
しかし、このコアを2、4個と複数利用できるようにコードを書くと 理想的には処理時間が1/2、1/4になります。
NumPyは、もちろん、複数のコアを利用できるように書かれています。
そのため、さきほどNumba化した関数もマルチコアが活かせるように 並列処理に書きかえます。
Numbaの機能の中に、処理の並列化もあるため、結構簡単に並列処理に変更できます。
- 並列処理化
@numba.njit(parallel=True) def summation_parallel( input_arr ): sum_val = 0 for i in numba.prange(len(input_arr)): sum_val += input_arr[i] return sum_val
前回からの変更点
- デコレータ@numba.jit
を@numba.njit(parallel=True)
に変更
- 関数内のfor文のレンジを表す部分をnumba.prange(len(input_arr))
に変更
- 並列処理を行うfor文は、prange
を使う必要があるみたい、、
NumPyとNumbaの比較
一回目の評価結果に追記する形式で行う。
- 計測環境
- 計測条件
- 入力要素数:256M(=256 * 1024 * 1024)
- 入力配列:NumPy配列
関数 | 処理時間[sec] | 高速化率(基準の何倍速いか) |
---|---|---|
基準:forループ実装 | 143.377 | - |
組み込み関数sum |
64.546 | 2.22 |
NumPyのsum 関数 |
0.144 | 995.67 |
forループ実装+Numba | 0.354 | 405.01 |
forループ実装+Numba(parallel) | 0.093 | 1541.68 |
Numbaの並列処理機能を利用すると、NumPyの総和計算より速くなりました!
(今回使ったPCに限った話かも、、、NumPyが遅いとは信じがたい、、、使っているコンパイラの違いとか??)
なんにせよ!
NumPyと同等以上の速度の総和計算関数が作れたので満足です!
まぁ、実際使うなら、NumPyのほうが簡単に使えるのでNumPyで十分な気がします!
最後に、計測に使ってプログラム(+Numba)
import time import numpy as np import numba @numba.njit def summation( input_arr ): sum_val = 0 for item in input_arr: sum_val += item return sum_val @numba.njit(parallel=True) def summation_parallel( input_arr ): sum_val = 0 for i in numba.prange(len(input_arr)): sum_val += input_arr[i] return sum_val N = 1024* 1024 * 256 input_arr = np.random.rand(N) print('Init : done (# of elements : ', N, ' )\n', flush=True) print('###### Simple loop summation ######') st = time.time() sum_val = 0 for item in input_arr: sum_val += item elapsed_time = time.time() - st print('\tSummation result\t: ', sum_val) print('\tElapsed time[sec]\t: ', elapsed_time) print('###### Default sum function ######') st = time.time() sum_val = sum(input_arr) elapsed_time = time.time() - st print('\tSummation result\t: ', sum_val) print('\tElapsed time[sec]\t: ', elapsed_time) print('###### Numpy sum function ######') st = time.time() sum_val = np.sum(input_arr) elapsed_time = time.time() - st print('\tSummation result\t: ', sum_val) print('\tElapsed time[sec]\t: ', elapsed_time) print('###### Simple loop summation with Numba ######') sum_val = summation(np.ones(1)) st = time.time() sum_val = summation(input_arr) elapsed_time = time.time() - st print('\tSummation result\t: ', sum_val) print('\tElapsed time[sec]\t: ', elapsed_time) print('###### Simple loop summation with Numba + parallel ######') sum_val = summation_parallel(np.ones(1)) st = time.time() sum_val = summation_parallel(input_arr) elapsed_time = time.time() - st print('\tSummation result\t: ', sum_val) print('\tElapsed time[sec]\t: ', elapsed_time)