目次

3. 配列計算,NumPy,Numba

3.1 NumPyとNumbaを用いたベクトル和とベクトル内積の計算

NumPyとNumbaを用いてベクトル和とベクトル内積を計算し、 配列計算の基本的な特性を調べます。
リスト3-1にベクトル和とベクトル内積のソースコードを示します。
計算方法として表3-1の3ケースを考えます。

表3-1 計算方法
計算方法No.計算方法の詳細
0 単純なfor文
1 NumPyのndarrayを用いた配列演算(以下、配列演算)
2 Numbaを用いたJITコンパイラー(以下、Numba)

リスト3-1 ベクトル和とベクトル内積のソースコード (vector.py)


"""
vector.py
test program of vector operations (NumPy and Numba)
"""

import time
import numpy as np
from numba import jit, prange, set_num_threads

# (vadd-0) sum of two vectors : for version
def vadd_for(a, b, c):
    n = len(a)
    for i in range(n):
        c[i] = a[i] + b[i]

# (vadd-1) sum of two vectors : array version
def vadd_array(a, b, c):
    c[:] = a + b

# (vadd-2) sum of two vectors : NumPy version
def vadd_numpy(a, b, c):
    c[:] = np.add(a, b)

# (vadd-3) sum of two vectors : Numba version
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def vadd_numba(a, b, c):
    n = len(a)
    for i in prange(n):
        c[i] = a[i] + b[i]

# (sdot-0) scalar product of two vectors : for version
def sdot_for(a, b):
    n = len(a)
    s = 0
    for i in range(n):
        s += a[i] * b[i]
    return s

# (sdot-1) scalar product of two vectors : array version
def sdot_array(a, b):
    return sum(a * b)

# (sdot-2) scalar product of two vectors : NumPy version
def sdot_numpy(a, b):
    return np.dot(a, b)

# (sdot-3) scalar product of two vectors : Numba version
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def sdot_numba(a, b):
    n = len(a)
    s = 0
    for i in prange(n):
        s += a[i] * b[i]
    return s

# parameters
N = 10000000
L = 1000
thread = 8
dtype = 'f4'   # 'f4' or 'f8'
fn = 'vadd-2'
#fn = 'sdot-2'

# Numba threads
if (fn == 'vadd-3') or (fn == 'sdot-3'):
    set_num_threads(thread)

t0 = time.time()

# setup
a = np.arange(N).astype(dtype)
b = np.arange(N).astype(dtype)
c = np.zeros(N, dtype)

t1 = time.time()

# calculation
for _ in range(L):
    if   fn == 'vadd-0':
        vadd_for(a, b, c)
    elif fn == 'vadd-1':
        vadd_array(a, b, c)
    elif fn == 'vadd-2':
        vadd_numpy(a, b, c)
    elif fn == 'vadd-3':
        vadd_numba(a, b, c)
    elif fn == 'sdot-0':
        s = sdot_for(a, b)
    elif fn == 'sdot-1':
        s = sdot_array(a, b)
    elif fn == 'sdot-2':
        s = sdot_numpy(a, b)
    elif fn == 'sdot-3':
        s = sdot_numba(a, b)

t2 = time.time()

# check
if fn.startswith('vadd'):
    r1 = np.sum(c)
    r2 = N * (N - 1)
else:
    r1 = s
    r2 = N * (N - 1) * (2 * N - 1) / 6

# output
print('(%s) N=%d, L=%d' % (fn, N, L))
print('%.2f+%.2f[sec], %e, %e' % (t1 - t0, t2 - t1, r1, r2))

# free
a = None
b = None
c = None

3.2 配列演算について

Pythonでは配列の演算はリスト3-1の関数vadd-1,sdot-1 のようにスカラーのように扱うことができ高速化することができます。 このようなプログラミングを配列指向プログラミングと呼びます。

3.3 NumPyについて

Pythonでは配列の演算はリスト3-1の関数vadd-2,sdot-2のように、 NumPyの関数を使用して計算することができます。
NumPyの関数は高度にチューニングされており、 また自動的にマルチコアを使用して並列処理を行います。

3.4 Numbaについて

Pythonでは前項のような配列演算ができないとき、 for文を用いてそのまま記述すると極めて計算が遅くなります。
そのようなときはNumbaを使用します。
Numbaではリスト3-1の関数vadd-3,sdot3のように関数の前に"@jit"行を挿入するだけで、 JIT(Just In Time)コンパイラーが起動してC言語に変換してコンパイルされ、 C言語と同等の速さになります。
@jit行には"(cache=True, nopython=True, nogil=True, parallel=True)" というオプションを指定することができます。それぞれの意味は以下の通りです。

Numbaを使用する注意点は以下の通りです。 (将来のバージョンでは変わる可能性があります)

3.5 計算時間

リスト3-1において、ベクトルの大きさNと繰り返し回数Lを変え、 計算方法にNumPyとNumbaをとり、Numbaについてはスレッド数を変えたときの計算時間を、 表3-2(ベクトル和)と表3-3(ベクトル内積)に示します。
計算時間は計算の本体部であり、配列を用意する部分は除いています。
計算時間の誤差を小さくするために繰り返し回数分だけ計算しています。
ベクトルの大きさNと繰り返し回数Lの積はNo.1~No.4で一定1010です。 従ってNo.1~No.4の演算量は同じです。
最適化の効果が見られるケースに色をつけています。

表3-2 ベクトル和の計算時間(実数単精度)
No.ベクトルの大きさN繰り返し回数Lvadd-1
(配列演算)
vadd-2
(NumPy)
vadd-3 (Numba)
1スレッド2スレッド4スレッド8スレッド16スレッド
110,000,0001,000 8.35秒 8.39秒 3.71秒 3.34秒 3.80秒 3.99秒 4.11秒
21,000,00010,000 6.45秒 6.56秒1.12秒0.70秒0.46秒0.40秒0.42秒
3100,000 100,0001.67秒1.68秒1.25秒0.73秒0.60秒0.57秒0.78秒
410,0001,000,000 2.21秒 2.26秒1.72秒 2.02秒 2.49秒 2.85秒 4.99秒

表3-3 ベクトル内積の計算時間(実数単精度)
No.ベクトルの大きさN繰り返し回数Lsdot-1
(配列演算)(注1)
sdot-2
(NumPy)
sdot-3 (Numba)
1スレッド2スレッド4スレッド8スレッド16スレッド
110,000,0001,000428秒1.71秒6.18秒3.23秒1.75秒1.70秒1.78秒
21,000,00010,000441秒0.61秒6.12秒3.18秒1.73秒1.01秒0.68秒
3100,000 100,000421秒0.63秒6.24秒3.30秒1.91秒1.24秒1.12秒
410,0001,000,000423秒1.21秒6.93秒4.64秒 3.89秒 3.61秒 5.52秒

(注1)1/100の繰り返し回数時の計算時間を100倍した推定値

表3-2~表3-3から以下のことがわかります。

以上から、アルゴリズム、問題のサイズ、キャッシュのサイズによって最適な方法が変わります。 複数の実装を行って比較することが望ましいです。

3.6 for文の計算時間

Pythonではfor文を用いてそのまま記述すると極めて計算が遅くなります。
表3-4にfor文のベクトル和(vadd-0)とベクトル内積(sdot-0)の計算時間を示します。
表3-2、表3-3と比べて繰り返し回数Lが1/1000となっていることに注意してください。

表3-4 for文のベクトル和とベクトル内積の計算時間(実数単精度)
No.ベクトルの大きさN繰り返し回数Lベクトル和
vadd-0
ベクトル内積
sdot-0
110,000,00011.26秒1.34秒
21,000,000101.26秒1.34秒
3100,000 1001.26秒1.34秒
410,0001,0001.26秒1.34秒

表3-4から以下のことがわかります。

Pythonでは計算の主要部のfor文は本節の方法を用いて高速化することが必須です。