パフォーマンスの最適化
1. 学習内容
- ❶ ベクトル化 — パフォーマンス向上の鉄則
- ❷ メモリ配置:C型連続配置とFortran型連続配置
- ❸ インプレース操作とコピー
- ❹ NumPy コードのプロファイリング
- ❺ パフォーマンスに関するよくある落とし穴
2. 主要な概念
(1) ベクトル化
PYTHON
import numpy as np
import time
n = 10_000_000
a = np.random.rand(n)
b = np.random.rand(n)
# Slow: Python loop
t0 = time.time()
c = np.empty(n)
for i in range(n):
c[i] = a[i] + b[i]
t1 = time.time()
# Fast: vectorized
t2 = time.time()
d = a + b
t3 = time.time()
print(f"Loop: {t1-t0:.3f}s, Vectorized: {t3-t2:.6f}s")
print(f"Speedup: {(t1-t0)/(t3-t2):.0f}x")
(2) インプレース操作
PYTHON
# Avoid copies with out= parameter
a = np.random.rand(1000000)
b = np.random.rand(1000000)
# Copy (allocates new memory)
c = a + b
# In-place (no allocation)
np.add(a, b, out=a)
(1) ▶ サンプル:ベクトル化による処理速度の比較(難易度 ⭐⭐)
PYTHON
import numpy as np
import time
n = 1_000_000
a = np.random.rand(n)
b = np.random.rand(n)
# Slow: Python loop
t0 = time.time()
c = np.empty(n)
for i in range(n):
c[i] = a[i] + b[i]
t1 = time.time()
# Fast: vectorized
t2 = time.time()
d = a + b
t3 = time.time()
loop_time = t1 - t0
vec_time = t3 - t2
print(f"Loop: {loop_time:.3f}s, Vectorized: {vec_time:.6f}s")
print(f"Speedup: {loop_time/vec_time:.0f}x")
出力:
TEXTループ実行時間:0.312秒、ベクトル化実行時間:0.002秒 処理速度の向上:156倍
(2) ▶ サンプル:out= を使用したインプレース演算(難易度 ⭐⭐)
PYTHON
import numpy as np
a = np.array([10, 20, 30, 40, 50])
b = np.array([1, 2, 3, 4, 5])
# Without out: creates new array
c = np.add(a, b)
print("Without out:", c)
# With out: reuses existing memory
result = np.empty(5)
np.add(a, b, out=result)
print("With out:", result)
# Chain operations in-place
np.multiply(result, 2, out=result)
np.add(result, 1, out=result)
print("Chained:", result)
出力:
TEXT除外:[11 22 33 44 55] 除外:[11 22 33 44 55] チェーン:[23 45 67 89 111]
(3) ▶ サンプル:メモリ配置の影響(難易度 ⭐)
PYTHON
import numpy as np
# C-contiguous (row-major) — default
c = np.array([[1, 2, 3], [4, 5, 6]])
print("C-contiguous:", c.flags.c_contiguous)
print("Fortran-contiguous:", c.flags.f_contiguous)
# Convert to Fortran-contiguous (column-major)
f = np.asfortranarray(c)
print("Fortran-contiguous:", f.flags.f_contiguous)
# Shape and strides
print("C strides:", c.strides)
print("F strides:", f.strides)
出力:
TEXTC-連続: True Fortran-contiguous: False Fortran-contiguous: True Cのストライド:(24, 8) Fの歩数:(8, 24)
❓ よくある質問
Q パフォーマンスに関して最も重要なルールは何か?
A Pythonのループは避けること。ループ(
for i in range(n))を書くたびに、「これをベクトル化できるか?」と自問してください。もし可能なら、NumPyを使えば10~100倍速くなります。Q C順とFortran順の違いは何ですか?
A C順(行優先)では、行が連続して格納されます。Fortran順(列優先)では、列が連続して格納されます。キャッシュのパフォーマンスを最大限に高めるには、アクセスパターンに合った順序を使用してください。
Q
out= パラメータはどのような場合に使用すべきですか?A 同じ大きな配列に対して多数の演算を行う場合、
out= を使用することで、演算ごとに新しいメモリを割り当てることを回避できます。ループやパイプライン内で使用してください。📖 まとめ
- ベクトル化こそがパフォーマンス向上の鉄則です。Pythonのループは絶対に避けるべきです。
- メモリ上の操作を行う際は、割り当てを回避するために
out=パラメータを使用してください - デフォルトは C-contiguous(行優先)です。アクセスパターンとメモリレイアウトを一致させてください
- Jupyter では
%timeit、スクリプトではtime.perf_counter()を使用したプロファイル - よくある落とし穴:ループ、不要なコピー、転置後のメモリの非連続性
📝 練習問題
-
初心者向け(難易度 ⭐):1,000万個の要素に対して
a + bを計算する時間を、ループを使用した場合とベクトル化を使用した場合で比較してください。速度向上率を記録してください。 -
中級(難易度 ⭐⭐):
a + bを使用した場合と使用しない場合で、a + bの実行時間を比較してください。割り当てのオーバーヘッドの差を測定してください。 -
上級 (難易度 ⭐⭐⭐): 大きな配列を作成し、それを転置してから、ある軸に沿って合計を求めます。転置した配列と連続したコピーとのパフォーマンスを比較し、その違いを説明してください。