実装方式
FFT を素直に実装すると計算量の導出に書いたような処理になる。 ただしこれらのコードは並べ替えのために単純にコピーを行ったり同じ $\omega$ の計算を繰り返し行ったりするなど効率がよくない。 そのため一般的にはそのような無駄を取り除いた方式の実装を FFT の実装として考えることが多い。 ここでは広く知られている 2 種類の実装方式を紹介する。
プログラムを実装する際にどちらの方が良いかは細かな実装や ユーザの需要次第となるので決定的な言及はできないが、 大まかには小メモリ動作を目指すなら In-place 型を、 速さを求めるなら Self-sorting 型を使用する印象である。
様々に提案されている FFT の各種高速化手法については基本的に 実装方法がどちらの方式であっても適用することができる。 In-place 型 FFT での効率化実装については[JW06]が詳しいので、 本サイトでは Self-sorting 型 FFT を用いて説明している。
Stockham 型 FFT / self-sorting 型 FFT
冒頭部分で述べたような、再帰呼び出しや同じ $\omega$ の計算を素直に省いた実装を Self-sorting 型 FFT、もしくは開発者の名をとって Stockham 型 FFT という。 Self-sorting の名の通りデータが常に並んでいるため計算がしやすい。 特徴としてデータ整列のために元のデータと同程度のデータ領域を必要とする。 領域 A → 領域 B → 領域 A →… と計算結果を交互に置いていくので Ping-pong FFT とも言われる。
時間間引き FFT
void SelfSortDITCore(int n, int m, Complex* x, Complex* y) { const int l = n / m / 2; for (int k = 0; k < m; ++k) { double wt = -M_PI * k / m; Complex w = Complex::polar(0, wt); for (int j = 0; j < l; ++j) { int xi0 = j * m + k, xi1 = xi0 + n / 2; int yi0 = j * 2 * m + k, yi1 = yi0 + m; Complex wx1 = x[xi1] * w; y[yi1] = x[xi0] - wx0; y[yi0] = x[xi0] + wx0; } } } void SelfSortDIT(int n, Complex* x, Complex* y) { for (int m = 1, i = 0; m < n; m *= 2, ++i) { if (i % 2) { SelfSortDITCore(n, m, y, x); } else { SelfSortDITCore(n, m, x, (m == n / 2) ? x : y); } } }
周波数間引き FFT
void SelfSortDIFCore(int n, int m, Complex* x, Complex* y) { const int l = n / m / 2; for (int k = 0; k < m; ++k) { double wt = -M_PI * k / m; Complex w = Complex::polar(0, wt); for (int j = 0; j < l; ++j) { int xi0 = j * 2 * m + k, xi1 = xi0 + m; int yi0 = j * m + k, yi1 = yi0 + n / 2; Complex t0 = x[xi0] + x[xi1]; Complex t1 = (x[xi0] - x[xi1]) * w; y[yi0] = t0; y[yi1] = t1; } } } void SelfSortDIF(int n, Complex* x, Complex* y) { int m = n / 2; if (static_cast<int>(log2(n)) % 2) { SelfSortDIFCore(n, m, x, x); m /= 2; } for (int i = 0; m >= 1; m /= 2, ++i) { if (i % 2) { SelfSortDIFCore(n, m, y, x); } else { SelfSortDIFCore(n, m, x, y); } } }
Cooley-Tukey 型 FFT / in-place 型 FFT
計算量の導出に書かれたコードを見直すと、 計算の前後 (DIT では前、DIF では後ろ) にデータの並べ替えのみを行っている 部分があることが分かる。 これは事前にデータの移動先、もしくは元のデータ位置が分かっていれば 並べ替えの処理、ならびに並べ替えのデータ領域が必要ない事を示していて、 実際に (2 基底の FFT では) ビット逆順 (bit reversal order) と呼ばれる順序に並び替えることで実現することができる。 この方式に基づいてメモリ領域の無駄を省いた実装を in-place 型 FFT、 もしくは Cooley-Tukey 型 FFT という。 一般にこの方式では計算前後にデータの並べ替え処理を行う必要があるが、 畳み込み乗算への応用のみを考える場合は 並べ替えない方針を取ることもできる。
以下に bit reverse の順序に並べ替えるコードを書いておく。
void BitReverse(int n, Complex* x) { for (int i = 1, j = 0; i < n; ++i) { for (int b = n >> 1; b; b >>= 1) { j ^= b; if (j & b) break; } if (i < j) swap(x[i], x[j]); } }
時間間引き FFT
void InPlaceDIT(int n, Complex* x) { BitReverse(n, x); for (int m = 1; m < n; m *= 2) { for (int k = 0; k < m; ++k) { double wt = -M_PI * k / m; Complex w = Complex::polar(0, wt); for (int j = 0; j < n; j += 2 * m) { Complex &x0 = x[k + j], &x1 = x[k + j + m]; Complex x1w = x1 * w; x1 = x0 - x1w; x0 = x0 + x1w; } } } }
周波数間引き FFT
void InPlaceDIF(int n, Complex* x) { for (int m = n / 2; m >= 1; m /= 2) { for (int k = 0; k < m; ++k) { Complex w = Complex::polar(0, wt); for (int j = 0; j < n; j += 2 * m) { Complex &x0 = x[k + j], &x1 = x[k + j + m]; Complex x1w = (x0 - x1) * w; x0 = x0 + x1; x1 = x1w; } } } BitReverse(n, x); }