トップページに戻る

高速化Tips

高速化Tips

Pythonを科学技術計算に使う人に向けてのヒント

プログラムが遅いと感じたらどうする? 「よし、とりあえずforループを全部FortranやCに書き換えて・・・」

↑は間違いです! まずはボトルネックを調べるべき

もし全てのコードを高速化しようとしたら Pythonの利点であるコーディング時間の短さを失う!

コーディング時間+計算時間=研究時間

ボトルネックを見つける方法

  • %timeit
  • %run -p
  • line_profiler (行ごとの計算時間を計測)

ボトルネックを見つけたら?

ボトルネックの種類を見極める

  1. Python”だから”遅い
  2. Python”でなくても”遅い

1.Python”だから”遅い

(例)成分ごとにforループを回している

高速化のためのライブラリが存在する

Numba Cython ctypes ↑に共通するのはコンパイルするということ!

2.Python”でなくても”遅い

例えば

  • 巨大な行列演算
  • 遅いアルゴリズムを使っている

Numpyは並のC言語のコードよりも速い この場合、CやFortranに書き換えたとしても劇的に早くなることはない

「Python”だから”遅い」ときの対処法 どのくらい遅いか(sumの場合)

In [1]:
import numpy as np

# 1000×1000の二次元配列
A = np.random.randn(1000, 1000)

Numpyの関数を使った場合

In [2]:
%timeit A.sum()
350 µs ± 7.46 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

生のPythonで書いた場合

In [3]:
def my_sum(arr2d):
    I = arr2d.shape[0]
    J = arr2d.shape[1]
    res = 0.
    for i in range(I):
        for j in range(J):
            res += arr2d[i, j]
    return res
            
%timeit my_sum(A)
180 ms ± 2.29 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

2桁ぐらい遅い!

Numbaを使ってみる

Numbaは、JIT(Just In Time)コンパイラ 初めての実行時にコンパイルを行い、それ以降はコンパイルされたものを使う。

(Just In Time = 必要になるときに必要な分だけ)

In [4]:
from numba import jit
import numba as nb
In [5]:
@jit
def my_sum_Numba(arr2d):
    I = arr2d.shape[0]
    J = arr2d.shape[1]
    res = 0.
    for i in range(I):
        for j in range(J):
            res += arr2d[i, j]
    return res

# 実行時に初めてコンパイル
my_sum_Numba(np.random.randn(*A.shape))

%timeit my_sum_Numba(A)
1.16 ms ± 21.1 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

Numpyのsum関数より少し遅いが、生Pythonよりは100倍以上速い

行列積の場合

In [6]:
A = np.random.randn(100, 30)
B = np.random.randn(30, 1000)
C = np.empty((1000, 1000))
            
%timeit np.dot(A, B)
164 µs ± 11.9 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

生Pythonで行列積

In [7]:
def my_mul(A, B):
    M_A, N = A.shape
    N_B = B.shape[1]
    C = np.zeros((M_A, N_B))
    for i in range(M_A):
        for j in range(N_B):
            for k in range(N):
                C[i, j] += A[i, k] * B[k, j]
    return C

%timeit my_mul(A, B)            
1.7 s ± 8.13 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

数桁遅い

Numbaで行列積

In [8]:
# @jit(nb.float64[:,:](nb.float64[:,:], nb.float64[:,:]), nopython=True)
@jit
def my_mul_numba(A, B):
    M_A, N = A.shape
    _, N_B = B.shape
    C = np.zeros((M_A, N_B))
    for i in range(M_A):
        for j in range(N_B):
            for k in range(N):
                C[i, j] += A[i, k] * B[k, j]
    return C

my_mul_numba(A, B)

%timeit my_mul_numba(A, B)
3.35 ms ± 27.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

高速化まとめ

  • まずは、Numpyの関数で書く!
  • どうしても生PythonループがボトルネックになっていたらNumba等を検討!

基礎編

応用編