Split-Radix FFT は,いかなる基数の Cooley-Tukey FFT よりも 浮動小数点演算回数が少ないアルゴリズムとして知られています [参考文献]. Split-Radix FFT の説明の前に,なぜ大きい基数の FFT の演算量が 削減されるのかについて少し考えてみます.具体例として基数 4 の FFT の バタフライの構造について調べます.基数 4 の FFT は,基数 2 の FFT の 四つのバタフライ演算を,一つにまとめたものに相当します.このとき, 四つあった乗算は,三つに減ります.この変形は次のようになります.
![]() |
図1.2.4-1. 基数 2 と基数 4 のバタフライ |
---|
この演算量の削減は,下半分のデータのバタフライ演算での,重なった 回転因子を一つにまとめることで行っていることがわかります.一方, 上半分のデータのバタフライ演算では演算の削減は行われておらず, 無意味な展開をしていることになります.この無意味な展開を削除したものが Split-Radix FFT アルゴリズムです.
Split-Radix FFT アルゴリズムは,偶数の添字の項に対しては,基数 2 の 分解を用い,奇数の添字の項に対しては,基数 4 の分解を用います. すなわち,DFT の基本式
N-1 jk -2πi / N A = Σ a W , W = e k j=0 j N N
を偶数項に関しては
N/2-1 jk A = Σ (a + a ) W 2k j=0 j N/2+j N/2
と分解し,奇数項に対しては
N/4-1 j jk A = Σ (a − a − i(a − a )) W W 4k+1 j=0 j N/2+j N/4+j 3N/4+j N N/4 N/4-1 3j jk A = Σ (a − a + i(a − a )) W W 4k+3 j=0 j N/2+j N/4+j 3N/4+j N N/4
と分解します.この分解による Split-Radix FFT は L 字の形をした,異なる 深さのバタフライ演算で構成されます.このため,log(N) 段ある演算は, 単純に一段ずつ終了させることができず,構造が多少複雑になります.例えば, N = 8 の場合のフローグラフは次のようになります.
![]() |
図1.2.4-2. Split-Radix FFT のデータフロー |
---|
この L 字のバタフライ演算は,基数 2 の分解の部分では一段階しか進まず, 基数 4 の分解の部分で二段階進みます.また,最後の段で演算を完了させる ために,基数 2 の演算が別に必要になります.
次に,周波数間引きの Split-Radix FFT のルーチン例を示します.
リスト1.2.4-1. Split-Radix FFT |
---|
#include <math.h> void fft(int n, double ar[], double ai[]) { int m, mq, i, j, j1, j2, j3, k; double theta, w1r, w1i, w3r, w3i; double x0r, x0i, x1r, x1i, x3r, x3i; /* ---- L shaped butterflies ---- */ theta = -8 * atan(1.0) / n; for (m = n; m > 2; m >>= 1) { mq = m >> 2; for (i = 0; i < mq; i++) { w1r = cos(theta * i); w1i = sin(theta * i); w3r = cos(theta * 3 * i); w3i = sin(theta * 3 * i); for (k = m; k <= n; k <<= 2) { for (j = k - m + i; j < n; j += 2 * k) { j1 = j + mq; j2 = j1 + mq; j3 = j2 + mq; x1r = ar[j] - ar[j2]; x1i = ai[j] - ai[j2]; ar[j] += ar[j2]; ai[j] += ai[j2]; x3r = ar[j3] - ar[j1]; x3i = ai[j3] - ai[j1]; ar[j1] += ar[j3]; ai[j1] += ai[j3]; x0r = x1r - x3i; x0i = x1i + x3r; ar[j2] = w1r * x0r - w1i * x0i; ai[j2] = w1r * x0i + w1i * x0r; x0r = x1r + x3i; x0i = x1i - x3r; ar[j3] = w3r * x0r - w3i * x0i; ai[j3] = w3r * x0i + w3i * x0r; } } } theta *= 2; } /* ---- radix 2 butterflies ---- */ for (k = 2; k <= n; k <<= 2) { for (j = k - 2; j < n; j += 2 * k) { x0r = ar[j] - ar[j + 1]; x0i = ai[j] - ai[j + 1]; ar[j] += ar[j + 1]; ai[j] += ai[j + 1]; ar[j + 1] = x0r; ai[j + 1] = x0i; } } /* ---- unscrambler ---- */ i = 0; for (j = 1; j < n - 1; j++) { for (k = n >> 1; k > (i ^= k); k >>= 1); if (j < i) { x0r = ar[j]; x0i = ai[j]; ar[j] = ar[i]; ai[j] = ai[i]; ar[i] = x0r; ai[i] = x0i; } } } |
このルーチンには,W=1,exp(πi/4) の場合に不要な乗算が含まれます. これらの不要な演算を取り除くためには,場合分けを行い,さらに二つの バタフライを追加します.基数が 2 のベキ乗の FFT の場合は 5 個の バタフライが必要ですが,Split-Radix FFT の場合は 3 個のバタフライで 済みます.さらに,高速化するためには内側のループでデータ参照を 連続にします.次に,これらの変更を施したルーチンの一例を示します.
リスト1.2.4-2. 3バタフライの Split-Radix FFT |
---|
#include <math.h> void fft(int n, double ar[], double ai[]) { int m, mq, irev, i, j, j1, j2, j3, k; double theta, w1r, w1i, w3r, w3i; double x0r, x0i, x1r, x1i, x3r, x3i; /* ---- scrambler ---- */ i = 0; for (j = 1; j < n - 1; j++) { for (k = n >> 1; k > (i ^= k); k >>= 1); if (j < i) { x0r = ar[j]; x0i = ai[j]; ar[j] = ar[i]; ai[j] = ai[i]; ar[i] = x0r; ai[i] = x0i; } } /* ---- L shaped butterflies ---- */ theta = -2 * atan(1.0) / n; for (m = 4; m <= n; m <<= 1) { mq = m >> 2; /* ---- W == 1 ---- */ for (k = mq; k >= 1; k >>= 2) { for (j = mq - k; j < mq - (k >> 1); j++) { j1 = j + mq; j2 = j1 + mq; j3 = j2 + mq; x1r = ar[j] - ar[j1]; x1i = ai[j] - ai[j1]; ar[j] += ar[j1]; ai[j] += ai[j1]; x3r = ar[j3] - ar[j2]; x3i = ai[j3] - ai[j2]; ar[j2] += ar[j3]; ai[j2] += ai[j3]; ar[j1] = x1r - x3i; ai[j1] = x1i + x3r; ar[j3] = x1r + x3i; ai[j3] = x1i - x3r; } } if (m == n) continue; /* ---- W == exp(-pi*i/4) ---- */ irev = n >> 1; w1r = cos(theta * irev); for (k = mq; k >= 1; k >>= 2) { for (j = m + mq - k; j < m + mq - (k >> 1); j++) { j1 = j + mq; j2 = j1 + mq; j3 = j2 + mq; x1r = ar[j] - ar[j1]; x1i = ai[j] - ai[j1]; ar[j] += ar[j1]; ai[j] += ai[j1]; x3r = ar[j3] - ar[j2]; x3i = ai[j3] - ai[j2]; ar[j2] += ar[j3]; ai[j2] += ai[j3]; x0r = x1r - x3i; x0i = x1i + x3r; ar[j1] = w1r * (x0r + x0i); ai[j1] = w1r * (x0i - x0r); x0r = x1r + x3i; x0i = x1i - x3r; ar[j3] = w1r * (-x0r + x0i); ai[j3] = w1r * (-x0i - x0r); } } /* ---- W != 1, exp(-pi*i/4) ---- */ for (i = 2 * m; i < n; i += m) { for (k = n >> 1; k > (irev ^= k); k >>= 1); w1r = cos(theta * irev); w1i = sin(theta * irev); w3r = cos(theta * 3 * irev); w3i = sin(theta * 3 * irev); for (k = mq; k >= 1; k >>= 2) { for (j = i + mq - k; j < i + mq - (k >> 1); j++) { j1 = j + mq; j2 = j1 + mq; j3 = j2 + mq; x1r = ar[j] - ar[j1]; x1i = ai[j] - ai[j1]; ar[j] += ar[j1]; ai[j] += ai[j1]; x3r = ar[j3] - ar[j2]; x3i = ai[j3] - ai[j2]; ar[j2] += ar[j3]; ai[j2] += ai[j3]; x0r = x1r - x3i; x0i = x1i + x3r; ar[j1] = w1r * x0r - w1i * x0i; ai[j1] = w1r * x0i + w1i * x0r; x0r = x1r + x3i; x0i = x1i - x3r; ar[j3] = w3r * x0r - w3i * x0i; ai[j3] = w3r * x0i + w3i * x0r; } } } } /* ---- radix 2 butterflies ---- */ mq = n >> 1; for (k = mq; k >= 1; k >>= 2) { for (j = mq - k; j < mq - (k >> 1); j++) { j1 = mq + j; x0r = ar[j] - ar[j1]; x0i = ai[j] - ai[j1]; ar[j] += ar[j1]; ai[j] += ai[j1]; ar[j1] = x0r; ai[j1] = x0i; } } } |
次に,Split-Radix FFT の演算量について考えます.長さ N の複素 DFT の計算に必要な実数乗算回数 μ と実数加算回数 α は次のように なります.
4 38 2 log N μ = --- N log N - ---- N + 6 + --- (-1) 3 9 9 8 16 2 log N α = --- N log N - ---- N + 2 - --- (-1) 3 9 9
これは不要な演算を取り除く 3 バタフライルーチンを用いて,複素乗算を 4 回の実数乗算と 2 回の実数加算で計算したときの値です.これは,基数 4 の FFT に比べて乗算回数が約 8/9 に減り,加算回数が約 32/33 に減ります. さらに,複素乗算を 3 回の実数乗算と 3 回の実数加算で計算する方法を用いると,
μ = N log N - 3N + 4 α = 3N log N - 3N + 4
となります.これは,基数 2 の FFT に比べて加算回数はそのままで 乗算回数を約半分にした値です.Split-Radix FFT は,通常の Cooley-Tukey 型 FFT よりも複雑なループ構造となります.しかし,浮動小数点演算量は他の いかなる基数の FFT よりも少なくなります.また,長さが 16 以下の 場合には,Split-Radix FFT は理論的に最適な演算回数となることが わかっています.