離散傅里葉變換 - 快速計算方法及C實現 - 第二篇

DFT – Fast algorithms and C implementations - Part2


Radix-2 DFT

在我的上一篇博客裏,已經介紹了基-2 離散傅里葉變換的基本原理(參見第4(4)部分),總結一下就是:

設N長序列\lbrace x_j \rbrace的DFT爲N長序列\lbrace X_k \rbrace,若N被2整除,則:

\forall ~k<N/2:~ \begin{cases} X_k = \sum_{j}{x_{2j}W_{N/2}^{jk}} + W_N^k \sum_{j}{x_{2j+1}W_{N/2}^{jk}}\\ X_{k+N/2} = \sum_{j}{x_{2j}W_{N/2}^{jk}} - W_N^k \sum_{j}{x_{2j+1}W_{N/2}^{jk}} \end{cases}

radix-2算法將一個N長的DFT分解爲兩個N/2長的DFT的線性組合。如果N/2仍然可以被2整除,則可以繼續對兩個N/2長的子序列進行奇偶分解。如果序列長度爲2的指數倍,即

\exists m \in \lbrace 0,1,2,...\rbrace, s.t., N=2^m

則可以應用radix-2算法將N長序列逐漸分解,直至序列長度變成1爲止。由於標量的DFT等於其自身(見我的上一篇博客),因此我們實際上已經不需要執行任何一次DFT變換——我們只需要執行幾次radix-2變換即可

下面從360圖書館借用兩張圖來說明這個計算過程:

上圖顯示的是N=16的DFT計算過程:基於radix-2算法,首先將序列分成偶數列和奇數列,由於兩個子序列長度爲8,可以進一步對各自進行奇偶分解,經過4次分解以後,我們得到了16組長度爲1的子序列,這些序列的DFT等於其自身。下面我們先根據radix-2算法求取8個N=2長序列[0,8], [4,2], [2,10], [6,14], [1,9], [5,13], [3,11], [7,15]的DFT,e.g.,

\mathcal{F} \left( \begin{bmatrix} x_0 \\ x_8 \end{bmatrix} \right ) = \begin{bmatrix} x_0+W_2^0 x_8 \\ x_0-W_2^0 x_8 \end{bmatrix}

然後再根據radix-2算法求取4個N=4長序列[0,4,8,12], [2,6,10,14], [1,5,9,13], [3,7,11,15]的DFT,e.g.,

\mathcal{F} \left( \begin{bmatrix} x_0 \\ x_4 \\ x_8 \\ x_{12} \end{bmatrix} \right ) = \begin{bmatrix} \mathcal{F} \left( \begin{bmatrix} x_0 \\ x_8 \end{bmatrix} \right ) + \begin{bmatrix} W_4^0 & ~ \\ ~ & W_4^1 \end{bmatrix} \mathcal{F} \left( \begin{bmatrix} x_4 \\ x_{12} \end{bmatrix} \right ) \\ ~ \\ \mathcal{F} \left( \begin{bmatrix} x_0 \\ x_8 \end{bmatrix} \right ) - \begin{bmatrix} W_4^0 & ~ \\ ~ & W_4^1 \end{bmatrix} \mathcal{F} \left( \begin{bmatrix} x_4 \\ x_{12} \end{bmatrix} \right ) \end{bmatrix}

然後再求N=8的兩個子序列的DFT,最後再求原始序列的DFT。

我見過不少radix-2 dft算法的代碼,都是基於遞歸函數來求解上述問題,這樣做的弊端是:

(1)每次都要進行一次序列的奇偶分離,通過不斷的memcpy,將N長序列分成兩個內存連續的N/2長序列。實際上頻繁地內存操作是高效率編程的大忌,由於現代CPU普遍速度較快,快到一眨眼就算完了,然後靜等內存操作完成,頻繁地內存操作對程序運行時間的影響越來越大,因此好的算法必須是對內存訪問次數越少越好,特別是內存寫數據的操作要越少越好。

(2)對於N較大的情況,遞歸層數較多,由於每次遞歸都要存儲前一層函數的狀態數據以及入口地址,進入遞歸函數時又要重新分配函數工作空間,因此深層次遞歸消耗很多內存以及片上高速緩存空間,大大影響程序性能。

我們再看看上面那張圖,如果一開始我們就將序列按照[0,8,4,12,2,10,6,14,1,9,5,13,3,11,7,15]的下標索引順序進行排列,又會如何?

構造新序列\lbrace{y_n}\rbrace

\begin{cases} y_0=x_0, y_1=x_8, y_2=x_4, y_3 = x_{12}, y_4 = x_2\\ y_5 = x_{10}, y_6=x_6, y_7=x_{14}, y_8 = x_1, y_9=x_9\\ y_{10}=x_5, y_{11}=x_{13}, y_{12}=x_3, y_{13} = x_{11}, y_{14}=x_{7}, y_{15}=x{15} \end{cases}

\mathcal{F} \left( \begin{bmatrix} x_0 \\ x_8 \end{bmatrix} \right ) = \begin{bmatrix} x_0+W_2^0 x_8 \\ x_0-W_2^0 x_8 \end{bmatrix} \Leftrightarrow \mathcal{F} \left( \begin{bmatrix} y_0 \\ y_1 \end{bmatrix} \right ) = \begin{bmatrix} y_0+W_2^0 y_1 \\ y_0-W_2^0 y_1 \end{bmatrix}

\mathcal{F} \left( \begin{bmatrix} x_0 \\ x_4 \\ x_8 \\ x_{12} \end{bmatrix} \right ) = \begin{bmatrix} \mathcal{F} \left( \begin{bmatrix} x_0 \\ x_8 \end{bmatrix} \right ) + \begin{bmatrix} W_4^0 & ~ \\ ~ & W_4^1 \end{bmatrix} \mathcal{F} \left( \begin{bmatrix} x_4 \\ x_{12} \end{bmatrix} \right ) \\ ~ \\ \mathcal{F} \left( \begin{bmatrix} x_0 \\ x_8 \end{bmatrix} \right ) - \begin{bmatrix} W_4^0 & ~ \\ ~ & W_4^1 \end{bmatrix} \mathcal{F} \left( \begin{bmatrix} x_4 \\ x_{12} \end{bmatrix} \right ) \end{bmatrix} \nonumber \Leftrightarrow

\mathcal{F} \left( \begin{bmatrix} y_0 \\ y_2 \\ y_1 \\ y_{3} \end{bmatrix} \right ) = \begin{bmatrix} \mathcal{F} \left( \begin{bmatrix} y_0 \\ y_1 \end{bmatrix} \right ) + \begin{bmatrix} W_4^0 & ~ \\ ~ & W_4^1 \end{bmatrix} \mathcal{F} \left( \begin{bmatrix} y_2 \\ y_3 \end{bmatrix} \right ) \\ ~ \\ \mathcal{F} \left( \begin{bmatrix} y_0 \\ y_1 \end{bmatrix} \right ) - \begin{bmatrix} W_4^0 & ~ \\ ~ & W_4^1 \end{bmatrix} \mathcal{F} \left( \begin{bmatrix} y_2 \\ y_3 \end{bmatrix} \right ) \end{bmatrix}

可以看出,對重排後的序列進行DFT,所有的下標變化似乎更有規律了,實際上也確實更加易於編程實現。

那麼究竟該按什麼樣的順序對下標進行重排呢?第二張圖給出了答案:

這個重排的方法叫作bit-reversal-order。也不知道是哪個牛人瞅了很久瞅出來的,還是用什麼厲害的數學方法推導出來的,不過就是這麼神奇:我們只要將原序列第k個數放置在第二個序列的第k'個位置上即可,其中k'與k是反位序關係(待會兒程序中會給出直觀解釋)。

Radix-2 DFT的程序實現

一切理論分析都是爲了那一段漂亮的代碼!

下面是我寫的基-2離散傅里葉變換的純C代碼,程序只實現了主要的idea,並未進行優化。

/* yufft.c
* This C source file implements the radix-2 DFT algorithm for 1D complex-to-complex forward Fourier transform.
* Written by: [email protected], Aug.14, 2019.
*/
#include <stdio.h>
#include <malloc.h>
#include <math.h>

typedef struct { float re, im; } ComplexFloat;

#define YU_COMPLEX_MUL(A, B, C) \
C.re = A.re * B.re - A.im * B.im; \
C.im = A.re * B.im + A.im * B.re
#define YU_COMPLEX_ADD(A, B, C) \
C.re = A.re + B.re; C.im = A.im + B.im
#define YU_COMPLEX_SUB(A, B, C) \
C.re = A.re - B.re; C.im = A.im - B.im
#define YU_COMPLEX_CONJ(A, B) \
B.re = A.re; B.im = -A.im

#define YU_PI 3.1415926535897932384626433832795

/*return 0 if failed, return 1 if success*/
int yuFFT(const float *src0, float *dst0, unsigned int len)
{
	const ComplexFloat *src = (ComplexFloat*)src0;
	ComplexFloat *dst = (ComplexFloat*)dst0;
	/*Step 1. Make sure len is power of 2*/
	unsigned int folds, i, j, k, tmp;
	for(folds = 0; folds < 32; folds++) {
		k = 1u << folds;
		if(len == k)
			break;
	}
	if(folds == 32u) {
		printf("len is not power of 2!");
		return 0;
	}
	/*len = 2^folds*/
	if(folds == 0) {
		dst[0] = src[0];
		return 1;
	}
	/*Step 2. rearrange data in bit reversal order*/	
	for(k = 0; k < len; k++) {
		/*order[k] = bit_reverse(k);*/
		/*k = sum_i (x_i * 2^i);
		* where x_i means the i-th bit of x.
		* order[k] = sum_i (x_(n-i) * 2^i;
		* where n = log2(len)-1 = folds-1*/
		/* sum_k (x_i * 2^i) = (x_0 << 0) | (x_1 << 1) | ...
		*/
		j = 0;
		for(i = 0; i < folds; i++) {
			/*(k & (1 << x)) >> x: get the x-th bit of k;
			x << i: equal to x*2^i */
			tmp = folds - 1u - i;
			/*j += ((k & (1u << tmp)) >> tmp) << i;*/
			j |= ((k & (1u << tmp)) >> tmp) << i;
		}
		//printf("k = %2d, j = %2d\n", k, j);
		dst[k] = src[j]; /*order[k] = j; dst[k] = src[order[k]];*/
	}
	/*Step 3. prepare dft coefficients*/
	ComplexFloat *coef = (ComplexFloat*)malloc(len * sizeof(ComplexFloat));
	double theta;
	for(k = 0; k < len; k++) {
		theta = -2.0 * YU_PI * k / len;
		coef[k].re = (float)cos(theta);
		coef[k].im = (float)sin(theta);
	}
	/*Step 4. recursive radix-2 dft*/
	/*N is the current dft size; N2 is half of N;
	* stride = len / N, thus: W_N^k = W_len^{k*stride} = coef[k*stride] */
	unsigned int N = 2, N2 = 1, stride = len / N;
	ComplexFloat *p, v;
	for(k = 0; k < folds; k++) {
		/*there are several ("stride") dft array share the same length of "N", 
		each array should be transformed independently*/
		for(i = 0; i < stride; i++) {			
			/*the start address of the current dft array*/
			p = dst + i * N;
			/*radix-2 transform*/
			for(j = 0; j < N2; j++, p++) {
				/*the dft coefficient of length N array: 
				W_N^j = W_{N*stride}^{j*stride}*/
				YU_COMPLEX_MUL(p[N2], coef[j * stride], v);
				YU_COMPLEX_SUB(p[0], v, p[N2]);
				YU_COMPLEX_ADD(p[0], v, p[0]);
			}
		}
		N *= 2; N2 *= 2; stride /= 2;
	}
	free(coef);
	return 1;
}

#undef YU_COMPLEX_MUL
#undef YU_COMPLEX_ADD
#undef YU_COMPLEX_SUB
#undef YU_PI

上述代碼中,Step 2步驟爲位反序算法,這個名字讀起來拗口,其實理解起來很容易:將一個整數的所有bit值按從低位到高位的順序重新排列,得到一個新的整數。

Step 3步驟求解傅里葉係數,一般應用中可以提前計算好傅里葉係數,然後保存起來,用的時候直接讀就好了。

PS:對上述代碼進行優化的一些建議:(1)提前計算好傅里葉係數以及bit-reversal-table,則每次進行fft的時候不需要重複計算,並可以避免在程序運行中不停地動態分配內存;(2)由於W_N^0 \equiv 1,因此在上述代碼中for循環的最後一層,可以將j=0單獨列出來;(3) 爲了進一步節省時間,可以在bit_reversal這一過程中使用更快的算法(主要還是查找表,見:https://stackoverflow.com/questions/746171/efficient-algorithm-for-bit-reversal-from-msb-lsb-to-lsb-msb-in-c);在計算傅里葉係數時,根據三角函數的性質可以減少調用cos、sin函數的次數

\begin{align} \left( W_N^{k+1} \right )_{re} &= \cos \left( \frac{-2\pi (k+1)}{N} \right ) \nonumber\\ &= \cos\left( \frac{-2\pi k}{N} \right ) \cos\left( \frac{-2\pi}{N} \right ) - \sin\left( \frac{-2\pi k}{N} \right ) \sin\left( \frac{-2\pi}{N} \right ) \nonumber\\ &= \left( W_N^k \right )_{re} \left( W_N^1 \right )_{re} - \left( W_N^k \right )_{im} \left( W_N^1 \right )_{im} \nonumber \end{align}

\begin{align} \left( W_N^{k+1} \right )_{im} &= \sin\left( \frac{-2\pi (k+1)}{N} \right ) \nonumber\\ &= \sin\left( \frac{-2\pi k}{N} \right ) \cos\left( \frac{-2\pi}{N} \right ) + \cos\left( \frac{-2\pi k}{N} \right ) \sin\left( \frac{-2\pi}{N} \right ) \nonumber\\ &= \left( W_N^k \right )_{re} \left( W_N^1 \right )_{im} + \left( W_N^k \right )_{im} \left( W_N^1 \right )_{re} \nonumber \end{align}

驗證程序

爲了測試上述代碼的正確性,我又寫了一個main函數,隨機生成數據用於變換,並調用了fftw庫來單獨計算一遍結果,最後比較兩個計算結果是否一致,代碼如下:

/* main.c
* This C source file tests the radix-2 DFT function and compares it with FFTW.
* Written by: [email protected], Aug.14, 2019.
*/
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <math.h>
#include <time.h>
#include "fftw3.h"

int yuFFT(const float *src, float *dst, unsigned int len);

int main()
{
	const int maxLength = 1024, nTrials = 3;

	/*Allocate memory*/
	float * const src = (float*)malloc(maxLength * 2 * sizeof(float));
	float * const dst = (float*)malloc(maxLength * 2 * sizeof(float));
	fftw_complex * const dsrc = (fftw_complex*)fftw_malloc(maxLength * sizeof(fftw_complex));
	fftw_complex * const ddst = (fftw_complex*)fftw_malloc(maxLength * sizeof(fftw_complex));

	for(int len = 1; len <= maxLength; len++) {
		/*Test if len is power of 2*/
		int k = (((len - 1) ^ len) + 1) >> 1;
		if(k != len)
			continue;

		/*Build fftw_plan*/
		int n[1] = { len };
		fftw_plan plan = fftw_plan_many_dft(1, n, 1, dsrc, 0, 1, 1, ddst, 0, 1, 1, FFTW_FORWARD, FFTW_ESTIMATE);

		for(int trial = 0; trial < nTrials; trial++) {
			/*Generate randoms between 0~1*/
			int *p = (int*)src, m = 0;
			srand((unsigned int)time(0));
			for(k = 0; k < len * 2; k++) {
				p[k] = rand();
				if(p[k] > m)
					m = p[k];
			}
			if(!m)
				continue;
			float scale = 1.f / m;
			for(k = 0; k < len * 2; k++)
				src[k] = p[k] * scale;
			/*Apply yufft*/
			yuFFT(src, dst, (unsigned int)len);
			/*Apply fftw to the same data (using double precision)*/
			double *src1 = (double*)dsrc, *dst1 = (double*)ddst;
			for(k = 0; k < len * 2; k++)
				src1[k] = (double)src[k];
			fftw_execute(plan);
			/*Compute the difference*/
			double re, im, dif = 0;
			for(k = 0; k < len * 2;) {
				re = dst1[k] - dst[k]; k++;
				im = dst1[k] - dst[k]; k++;
				re = re * re + im * im;
				if(dif < re)
					dif = re;
			}
			dif = sqrt(dif);
			printf("len = %4d, trial = %2d, dif = %g\n", len, trial, dif);
		}
		fftw_destroy_plan(plan);
	}

	free(src);
	free(dst);
	fftw_free(dsrc);
	fftw_free(ddst);
	getchar();
	return 0;
}

上述代碼的執行邏輯是:遍歷1~1024之間所有2的指數,然後生成對應長度的隨機序列,調用yufft算法,再調用fftw算法分別計算一次dft,比較二者之間的絕對差,並打印結果。

編譯和運行程序:在windows下比較交單,藉助visual studio等微軟的開發工具,從fftw官網下載動態鏈接庫,配置好以後就可以直接運行。在Linux下需要先安裝fftw(一行命令就夠了:sudo apt-get install fftw3 fftw3-dev pkg-config),然後建立如下的CMakeLists文件:

# CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
project(yufft)
add_executable(yufft main.c yufft.c)
target_link_libraries(yufft -lfftw3 -lm)

然後在程序目錄下打開終端,執行:

cmake
make
./yufft

我在Win10下和Ubuntu 16.04下都測試了,程序運行結果正確。

PS:在上述main.c文件中有一行代碼很有意思:

int k = (((len - 1) ^ len) + 1) >> 1;

這句話的作用就是將整數len中最大的2的指數項取出來賦值給k,爲什麼呢?假設len>1,我們將len的二進制表示中從右往左的第一個1單獨拎出來,則len的二進制表示爲:

...xxx1000...

上面的省略號表示1的左邊有任意個未知比特位,1的右邊有任意個0比特位. 則len-1的二進制表示就是:

...xxx0111...

求len與len-1的亦或(XOR,相同比特位的亦或結果是0,不相同比特位的亦或結果是1),故亦或結果爲:

...0001111...

可以看到,那些未知的比特位全部變成0了。我們再給它加個1,然後對結果右移一位,則得到:

...0001000...

們知道一個數若是power-of-2,則它的二進制表示中應該只有一個比特位是1,其它比特位都是0。另外一個數與一個2的指數相乘,則等價於將這個數左移。不難看出,(((len - 1) ^ len) + 1) >> 1的作用就是提取len中最大的那個可以表示成2的指數的那個因子。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章