数値計算でよく利用されている Python (CPython) ですが、for文の処理が遅いという問題点があります。
音響信号処理ではC/C++風に配列の各要素にアクセスが必要な処理を for文を使って実装することが多いため、これは致命的です。
そこで、Python用のjitコンパイラ numba
で(リアルタイム)音響信号処理をどれくらい高速化できるか、音響信号処理でよく使われる2次IIRフィルタを使って計測・比較してみました。
numbaについて
概要
A ~5 minute guide to Numba — Numba 0.56.4+0.g288a38bbd.dirty-py3.7-linux-x86_64.egg documentation
conda install numba
もしくは
pip install numba
で導入できます。
基本的な使い方としては、関数にデコレータ @jit
を付けることで実行時にjitコンパイルすることができます。
from numba import jit @jit def proc(arg0, arg1): #.... return res
単に@jit
を付けるだけでもOKですが、より高速化するためのオプション指定ができます。
型指定
引数や返り値の型を指定します。
信号処理用途であれば、
- void 返り値のないときに利用
- b1 boolean
- i8 int8
- f8 float64
- f8[:] float64 1次元配列
- f8[:,:] float64 2次元配列
はよく使うと思われます。
注意として、特に配列 (numpy.array)の場合は dtype
と一致するように与えます。
参考:
以下はオプションの与え方の例です。
# 返り値あり @jit(i8(f8[:],f8[:])) def proc(arg0, arg1): #.... return res # 返り値なし @jit(void(f8[:])) def proc2(arg0): #....
処理時間比較用スクリプト
一般的なリアルタイム音響信号処理を想定し、下記条件で計測・比較しました。
- 2次IIRフィルタに対して
@jit
の有無を比較 @jit
のオプションとしては型指定の有無を比較- 入力信号は約2分、サンプリング周波数 44.1 kHz相当の信号を与える
- 信号はサンプル数が
blocksize
のブロックごとに処理 - 10回の平均処理時間を算出
import time import numpy as np from numba import jit, f8, i8, void @jit(void(i8, f8[:], f8[:], f8[:], f8[:], f8[:], f8[:])) def compute_biquad_filter(length, x, y, x_buf, y_buf, b, a): """ a[0] == 1.0 """ for k in range(length): y[k] = b[0] * x[k]\ + b[1] * x_buf[1]\ + b[2] * x_buf[0]\ - a[1] * y_buf[1]\ - a[2] * y_buf[0] x_buf[0] = x_buf[1] x_buf[1] = x[k] y_buf[0] = y_buf[1] y_buf[1] = y[k] blocksize = 512 sr = 44100 duration = 120 # [sec] # test signal length = (sr * duration // blocksize) * blocksize n_blocks = length // blocksize x = np.random.normal(0.0, 0.1, length) # input: White noise y = np.zeros(x.shape) # output x_buf, y_buf = np.zeros(2), np.zeros(2) # 2-dim IIR filter coef (peaking filter) b = np.array([0.00473042, 0.00946083, 0.00473042]) a = np.array([ 1. , -1.84849692, 0.86741859]) n_trial = 10 elapsed_time_array = np.zeros(n_trial) for t in range(n_trial): start_time = time.time() for k in range(n_blocks): slc = slice(k*blocksize, (k+1)*blocksize) compute_biquad_filter(blocksize, x[slc], y[slc], x_buf, y_buf, b, a) elapsed_time_array[t] = time.time() - start_time print(f"trial {t}\t\telapsed time: {elapsed_time_array[t]:0.3f} [sec]") print(f"\n{np.mean(elapsed_time_array):0.3f},{np.std(elapsed_time_array, ddof=1):0.3f}\n")
測定環境
バージョンは Python 3.8.8 を利用しています。
計測結果
jitの有無 | 型指定 | 平均 | 偏差 | jit無しとの速度比 |
---|---|---|---|---|
× | × | 20.806 | 0.683 | - |
○ | × | 0.080 | 0.104 | 260倍 |
○ | ○ | 0.055 | 0.008 | 378倍 |
jitを利用しない場合は圧倒的に処理時間がかかることが分かりました。
私の環境だとjit無しでも2次IIRフィルタ1つであればリアルタイム処理は間に合うかもしれませんが、複数のフィルタを利用したり、より複雑な処理をするのは厳しそうです。
しかし、jit有りでは非常に高速に処理できているため、より複雑な信号処理でも対応できそうです。
また、jit有りの場合でも型指定をすることで1.45倍くらい処理時間を削ることができます。
偏差を見ても、処理時間のバラつきが少なくなるので、Pythonでfor文を使った音響信号処理をする時は型指定して@jit
利用すべきであるといえます。