C言語による実装

前回物理的な意味を考えた以下のFIRフィルターの式(畳み込み演算)をC言語で実装してみましょう。

畳み込み変換の式

この実装は様子見を目的とした試験的なものなので以下のような割り切ったものにします

上の式のNにあたる定数をFIRフィルターではタップ数と呼んでいます。インパルス応答を格納するためにh[TAP]という配列を用意します。また、過去TAPサンプルのデータを保存しておくためにdelay[TAP]という配列を用意します。delayという名前は、この変数がディレイ・ライン( delay line)と呼ばれることから名づけています。余談ですが人によってはこの変数を状態変数( state variable )と見ることもあります。

ディレイ・ラインの実装

ディレイ・ラインをどのように実装するかは、FIRフィルターの性能に直結する重要な問題です。配列に実装するのは当然として、データの並べ方で大きく性能が変わってきます。以下では、どのようなデータの配置方法があるかを考えてみます。

すぐに思いつく方法として最も古いデータを先頭に、最も新しいデータを最後尾に置くというものがあります。下の図はその様子をあらわしています。配列の先頭には最も古い(この場合はTAPサンプル前)のデータがあり、最後尾には最新の入力値があります。逆順にする方法もあり、一見そちらのほうがプログラムが簡単になりますが、図示した順序だと先の式とデータ構造の順序が同じになる利点があります。いずれにせよ本質的な話ではありません。

ディレイライン

この方式をプログラムに落とすと次のようになります

for ( i=0; i<TAP-1; i++ )
    sum += delay[TAP-1-i] * h[i];

十分に簡潔なプログラムですが大きな欠点があります。このプログラムが簡潔なのは畳み込みの間だけで、実際にはデータの更新時に大きなオーバーヘッドが起きます。delay配列の中のデータの並びは前提から常に最後尾が最新でなければなりません。ということは、新しいデータが来るたびに現在のデータを一つずつずらさなければならないのです。それは下のようなプログラムになります。

for ( i=0; i<TAP-2; i++ )
    delay[i] = delay[i+1];
delay[TAP-1] = newdata;

メモリーのリードとライトを行うと、下手をすると計算自身より時間がかかりかねません。そこでこのような非効率なことはせずに、書き込みの位置を次々にずらしていく方法がよく使われます。この様子を下に図示します。最初の図では最新のサンプルがdelay[3]に格納されています。その次のサンプルはdelay[4]に格納されます。この方法は書き込み位置をずらすことから、上の方法のようにデータ全体をずらす必要がありません。その代わりに配列の境界チェックが必要になります。というのは、書き込み位置が配列の端に来た場合、次の書き込みは配列の先頭に行わなければならないからです。これは書き込みだけでなく読み込みについても同じです。

サーキュラーバッファ

書き込みと読み込みの位置がぐるぐると配列上を巡ることから、この方式は循環バッファ( Circular Buffer )と呼ばれます。循環バッファの利点は先に述べたように1サンプルごとに全データをずらす必要がない点です。しかしデメリットもあります。この方式は1サンプル読むあるいは書くごとにその位置が配列の端に来たかどうかをチェックしなければなりません。そして端にきたら反対の端に移る作業が必要なのです。この結果、すべてのアクセスに対して条件検査が必要になり、多大なオーバーヘッドがかかるようになります。この方式をプログラムで表現してみると下のようになります。

for ( k=0; k<TAP-1; k++ )
{
    sum += delay[j--]*h[k];
    if ( j < 0 )
        j = TAP -1;
}
sum += delay[j]*h[TAP-1];

このように一見して非効率とわかるプログラムになります。ためしにこのプログラムをコンパイルしてみると、最適化してもループの中は11命令になりました。とても信号処理に使えるとは言えません。そもそも上の単純コピーのほうが速く走ります。

なぜ効率が悪いのか

「DSPは信号処理が得意」といわれるのに、なぜこうも結果が悪いのでしょうか。それはDSPではなくC言語が悪いのです。C言語はもともとUnixを記述するために作られたプログラミング言語です。必然的に言語の制御構造やデータ構造はOSでよく使われるものを意識しており、信号処理ははじめから眼中にありません。農夫に魚を捕らせるようなもので、評価をすること自体無理があります。

今回の例でいえば、もっともつらい点はCコンパイラがDSPの循環バッファ・アクセス機能を使っていないことです。それは仕方のないことで、C言語自身に循環バッファの意味記述をする機能がない以上コンパイラは手も足も出ません。人間ならばプログラムを読んでそれが循環バッファ・アクセスであると看破することもできます。しかしそれもよほど真剣に読んだ場合であって、上のようなコメントもないようなプログラムだとどんなに簡単でもかなり疲れる作業になります。ましてコンパイラが人間の書くさまざまな表現形式の循環バッファを見抜けるかというと、絶望的でしょう。

最近はDSP用のCコンパイラの性能も目を見張るほどよくなっています。また、DSP側にもCコンパイラを意識した機能が備わり始めています。ADSP-2191のリンケージ命令などもその一つです。しかしながら性能がよくなったといえるのは信号処理以外の制御コードの話であり、信号処理そのものをC言語で書くことは相変わらず非現実的です。

このように、信号処理コード自身はアセンブリ言語で書かなければならないと腹をくくっておく必要があります。無論、必要のないところまでアセンブリ言語で書くことはありませんが、やるときはやる、というのがDSPの世界です。

次は⇒アセンブリ言語による実装

2191空挺団 | プログラム | EZ-KIT | こぼれ話 | アーキテクチャー | 命令 | レジスタ | DSP掲示板 | FAQ |