プログラマーの徒然ブログ

プログラミングに関することをはじめ、興味がでたものを雑多に!

【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)

処理時間比較

  • 計測環境
    • ASUS ZenBook UX390U (ノートPC)
      • Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz
  • 計測条件
    • 入力要素数: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を使って高速に処理ができる機械語に変換します。

t49m1.hatenablog.com

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の比較

一回目の評価結果に追記する形式で行う。

  • 計測環境
    • ASUS ZenBook UX390U (ノートPC)
      • Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz
  • 計測条件
    • 入力要素数: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の比較

一回目の評価結果に追記する形式で行う。

  • 計測環境
    • ASUS ZenBook UX390U (ノートPC)
      • Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz
  • 計測条件
    • 入力要素数: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)