Speedup python script using numba
tags: python, research Numba というモジュールを利用することで,pythonスクリプトの数値演算のみからなる関数を劇的に高速化することができる. (数値演算でない部分は高速化されない.)
ただし,高速化する関数の書き方を気をつけないとその恩恵を受けられないので注意.
簡単な例
import numba
import numpy as np
def matmul1(a, b):
lenI = a.shape[0]
lenJ = a.shape[1]
lenK = b.shape[1]
c = np.zeros((lenI, lenJ))
for i in range(lenI):
for j in range(lenJ):
for k in range(lenK):
c[i, j] += a[i, k] * b[k, j]
return c
@numba.jit
def matmul_numba(a, b):
lenI = a.shape[0]
lenJ = a.shape[1]
lenK = b.shape[1]
c = np.zeros((lenI, lenJ))
for i in range(lenI):
for j in range(lenJ):
for k in range(lenK):
c[i, j] += a[i, k] * b[k, j]
return c
上記のように,普通に書いた関数と @numba.jit
を付けた関数とを用意して,速度比較する.
ndim = 100
a = np.random.randn(ndim,ndim)
b = np.random.randn(ndim,ndim)
%timeit matmul1(a,b)
%timeit matmul_numba(a,b)
1 loops, best of 3: 771 ms per loop
1000 loops, best of 3: 1.12 ms per loop
のように, @numba.jit
を付けたものの方が圧倒的に高速化されていることが分かる.
numbaによる高速化を阻害する要因
上記のように, @numba.jit
デコレータを付けるだけでかなりの高速化が望めるので,うれしいが,関数内にそれを阻害する要因があると,こうそくかされない.
numba
によるオーバーヘッドで少し遅くなるという結果に終わることもあるので注意.
例えば,下記のようなコードを考える(Coulombポテンシャルを直接法で計算する関数).
# Compute direct sum of Coulomb sum in the cubic region
import numba
import math
@numba.jit
def direct_cube(nc=5,pos=[],chgs=[],alc=10.):
phi = 0.0
r = np.zeros((3,),dtype=float)
rcut = (nc-1)*alc
li = np.zeros((3,),dtype=float)
ndim = len(chgs)
for ix in range(-nc+1,nc):
for iy in range(-nc+1,nc):
for iz in range(-nc+1,nc):
li[:] = [float(ix),float(iy),float(iz)]
for pi,zi in zip(pos,chgs):
r[:] = (-pi[:] -li[:])*alc
denom = np.linalg.norm(r)
phi += zi/denom
return phi,rcut
これの時間を測ってみると, :
%timeit phi = direct_cube(nc=5,pos=pos,chgs=charge,alc=alc)
1 loops, best of 3: 1.07 s per loop
ここで, pos
や chgs
は原子座標(pi=[xi,yi,zi])と原子当たり電荷(zi)であり,原子数個の次元を持っている(ここではあまり考える意味が無いので無視してよい).
これを @numba.jit
なしで実行すると, :
1 loops, best of 3: 941 ms per loop
となり,僅かに @numba.jit
なしの方が速い.
np.linalg.normの置き換え
まずは,最も内側のループ内の,
denom = np.linalg.norm(r)
の部分を,
denom = math.sqrt(r[0]*r[0]+r[1]*r[1]+r[2]*r[2])
のように,置き換える.すると, :
%timeit phi = direct_cube(nc=5,pos=pos,chgs=charge,alc=alc)
1 loops, best of 3: 565 ms per loop
しかし, @numba.jit
なしの場合, :
1 loops, best of 3: 493 ms per loop
に比べると,僅かに遅い.
zip関数の置き換え
for pi,zi in zip(pos,chgs):
の部分を,
for i in range(ndim):
pi = pos[i]
zi = chgs[i]
のように置き換えると, :
%timeit phi = direct_cube(nc=5,pos=pos,chgs=charge,alc=alc)
100 loops, best of 3: 11.6 ms per loop
のように劇的に速くなる.ちなみに, @numba.jit
なしの場合, :
1 loops, best of 3: 501 ms per loop
のようにあまり変わらない.
結論
zip
関数を使わない.(enumerate
は高速化する)- 最内側のループ内の関数がボトルネックの可能性があるので気をつける(今回の場合は
np.linalg.norm()
が問題)
ちなみに,
- 最内側の
if
分は高速化をあまり阻害しない kv[:] = [float(x) for x in (kx,ky,kz)]
のようなリスト内包表記があるとJITコンパイルできないらしく,kv[:] = [float(kx),float(ky),float(kz)]
のように書き換える必要がある.