6.19. パフォーマンス¶
numpy をカメラ上で高速に動作させるための設計判断(配列全体に対するライブラリ呼び出し、パックされた型付きバッファ、ソースとデータを共有するビュー)は、同時に知っておく価値のある一連の習慣も浮き彫りにします。形状とストライド のページでは最終軸のレイアウト規則をすでに扱いました。このページでは、ストリーミングループで最も重要となるアロケーションと dtype の習慣をまとめます。
6.19.1. 適切な dtype を選ぶ¶
すべてのコンストラクタのデフォルト dtype は float です。本来 8 ビットや 16 ビットであるデータ(ADC サンプル、画像ピクセル、センサーの読み取り値)に対しては、整数型のいずれかを dtype= で明示的に渡してください:
adc = np.array(adc_samples, dtype=np.uint16)
4 バイトの float デフォルトと比べて、RAM の節約は uint16 で 2 倍、uint8 で 4 倍になります。numpy 内部の整数コードパスは汎用的な float のものよりタイトなため、演算も高速に実行されます。Dtype で扱った整数オーバーフロー規則が適用されます。オーバーフローする可能性のある算術演算の前には、より広い型へキャストしてください。
6.19.2. イテラブルより ndarray を優先する¶
ほとんどのリダクションやユニバーサル関数は、イテラブルと ndarray のどちらも受け付けます:
np.sum([1, 2, 3, 4, 5]) # works, but slow
np.sum(np.array([1, 2, 3, 4, 5])) # ~3x faster
イテラブル形式では、numpy は入力を一度に 1 つの Python オブジェクトずつたどり、使用する前にそれぞれを数値へ変換せざるを得ません。ndarray に対しては変換がすでに済んでいるため、呼び出しはパックされたバッファをそのまま処理して進みます。
同じデータを複数回使う場合は、ndarray を一度だけ構築して渡し回してください。データが Python のリストとしてのみ存在し一度しか消費されない場合は、変換コストが高速化分を上回ることがあります。array() コンストラクタ自体がリストをたどってアロケーションを行う必要があるためです。
6.19.3. コピーよりビューを優先する¶
スライス、高ランク配列の単一軸インデックス、reshape()、transpose()、frombuffer() はいずれも、ソースとデータを共有する ビュー を返します。これらは事実上コストがかかりません。
copy()、flatten()、ブールインデックス(a[mask])、およびあらゆる算術式は コピー をアロケートします。これらに頼るのは、独立したバッファが本当に必要なときだけにしてください。
迷ったときは、ndinfo() が基盤バッファの位置を表示します。同じアドレスを報告する 2 つの配列はデータを共有しています。ビューとコピーの完全な対応表は ビューとコピー にあります。
6.19.4. 一度アロケートして、その後に書き込む¶
カメラ上で最大のパフォーマンス上の落とし穴は、毎秒何度も実行されるループの内側で新しい配列をアロケートすることです。新しい ndarray はそれぞれカメラに RAM を要求し、頻繁な新規アロケーションは RAM を浪費します。
ほとんどのユニバーサル関数は out= を受け付けるため、結果をすでに存在する配列へ書き込めます:
x = np.linspace(0, 2 * np.pi, num=512)
y = np.zeros(512) # allocate once
while True:
np.sin(x, out=y)
# use y ...
image.Image.to_ndarray() は同じ理由で buffer= を受け付けます。spectrogram() および from_int32_buffer() 系の変換関数は out= と scratchpad= の両方を受け付けます。すべてを一度だけアロケートして再利用してください。
6.19.5. インプレース演算子を使う¶
b = b + 1 は b と同じサイズの一時オブジェクトをアロケートし、コピーして再代入します。b += 1 は b を直接変更します:
# makes a temporary
b = b + 1
# no temporary
b += 1
同じ考え方が複合式にも当てはまります。a + b * c は b * c のための一時オブジェクトをアロケートします。式を事前アロケートしたバッファへ書き込む単純なサブ代入に分割すれば、一時オブジェクトを排除できます:
# one temporary for (a + b), another for the ``* 2``
out = (a + b) * 2
# zero temporaries
out[:] = a
out += b
out *= 2
6.19.6. 結果を構築する。追記しない¶
ndarray には append がありません。これは意図的なものです。配列を拡張するには、新しいより大きなバッファをアロケートし、古い内容をそこへコピーする必要があります。マイクロコントローラ上では、最終サイズを事前にアロケートして 埋めて ください:
out = np.zeros(N, dtype=np.float)
for i in range(N):
out[i] = some_calculation(i)
N が本当に事前に分からない場合は、Python の list に書き込み、最後に array() で一度だけ変換してください。
6.19.7. 新しい配列ではなくスライス代入を使う¶
「他の配列の断片から新しい配列を構築する」という多くのパターンは、呼び出しごとの新規アロケーションではなく、事前アロケートしたバッファへのスライス代入として表現できます。
サンプルのストリームに対するローリングウィンドウ(移動平均フィルタの基礎)が代表的な例です。バッファは最新の N サンプルを保持し、反復ごとに最も古いものを捨てて最も新しいものを追加します。素直な書き方では反復ごとにバッファを再構築します:
while True:
sample = read_sample()
buf = np.concatenate((buf[1:], # new buffer every loop
np.array([sample])))
avg = np.mean(buf)
これはサンプルごとの新規アロケーション、そして N - 1 個の要素のコピーです。スライス代入形式ではインプレースでシフトします:
N = 16
buf = np.zeros(N, dtype=np.float) # allocate once
while True:
sample = read_sample()
buf[:-1] = buf[1:] # shift left by one
buf[-1] = sample # append at the end
avg = np.mean(buf)
buf[:-1] = buf[1:] が興味深い行です。同じバッファへの 2 つの重なり合うビューであり、右辺のスライスは一方の端から読み取られ、もう一方の端へ書き込まれます。numpy はインプレースのシフトが安全になる順序で基盤メモリをたどります。ループの内側で新しい配列がアロケートされることは一切ありません。
6.19.8. ストリーミングループでのブールマスクに注意する¶
ブールインデックスと where() は呼び出しごとに新しい配列を生成します。結果のサイズはデータに依存するため、事前アロケートしたバッファではアロケーションを吸収できません。ストリーミングループで繰り返しマスクを構築すると、使い捨ての配列で RAM が埋まります。定期的な gc.collect() がその領域を回収します:
import gc
for i in range(1000):
mask = a < threshold
_ = a[mask]
if i % 100 == 0:
gc.collect()
同じ注意点が (a > lo) & (a < hi) のような複合ブール式にも当てはまります。各演算子が新しいブール配列をアロケートします。マスクを再利用する場合は、一度構築して保持してください:
mask = a < threshold
foo[mask] = 0
bar[mask] = 1