Wizard Notes

Python, JavaScript を使った音楽信号分析の技術録、作曲活動に関する雑記

Python:numba (jit)で音響信号処理の高速化 - 2次IIRフィルタの処理時間計測

数値計算でよく利用されている 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利用すべきであるといえます。

関連文献