OpenMP并行化傅里叶变换

设计思想

傅里叶变换,表示能将满足一定条件的某个函数表示成三角函数(正弦和/或余弦函数)或者它们的积分的线性组合。在不同的研究领域,傅里叶变换具有多种不同的变体形式,如连续傅里叶变换和离散傅里叶变换。
快速傅里叶变换 (Fast Fourier Transform),即利用计算机计算离散傅里叶变换(DFT)的高效、快速计算方法的统称,简称FFT,于1965年由J.W.库利和T.W.图基提出。
对多项式 f ( x ) = ∑ i = 0 n a i x i , g ( x ) = ∑ i = 0 n b i x i f(x)=∑_{i=0}^na_i x_i ,g(x)=∑_{i=0}^nb_i x_i f(x)=i=0naixi,g(x)=i=0nbixi,定义其乘积fg为 ( f g ) ( x ) = ( ∑ i = 0 n a i x i ) ( ∑ i = 0 n b i x i ) (fg)(x)=(∑_{i=0}^na_i x_i )(∑_{i=0}^nb_i x_i) (fg)(x)=(i=0naixi)(i=0nbixi)
显然我们可以以 O ( n 2 ) O(n^2) O(n2)的复杂度计算这个乘积的每一项的系数。
但FFT可以以 O ( n l o g n ) O(nlogn) O(nlogn)的时间复杂度来计算这个乘积。
快速傅立叶算法核心的思想是分治,即把一个复杂的问题,分解为一个小的类似问题进行求解。
假定待变换离散时间序列信号长度为 n = 2 m n=2^m n=2m,将X(n)按照奇偶分组:
X ( k ) = ∑ r = 0 N / 2 − 1 x ( 2 r ) W N 2 r k + ∑ r = 0 N / 2 − 1 x ( 2 r + 1 ) W N 2 r + 1 k X(k)=\sum_{r=0}^{N/2-1}x(2r) W_N^2rk+∑_{r=0}^{N/2-1}x(2r+1) W_N^{2r+1}k X(k)=r=0N/21x(2r)WN2rk+r=0N/21x(2r+1)WN2r+1k

上式可变换为:
X ( k ) = ∑ r = 0 N / 2 − 1 x ( 2 r ) W N / 2 r k + W N k ∑ r = 0 N / 2 − 1 x ( 2 r + 1 ) W N / 2 r k X(k)=∑_{r=0}^{N/2-1}x(2r) W_{N/2}^{rk} +W_N^k ∑_{r=0}^{N/2-1}x(2r+1) W_{N/2}^{rk} X(k)=r=0N/21x(2r)WN/2rk+WNkr=0N/21x(2r+1)WN/2rk

A ( k ) = ∑ r = 0 N / 2 − 1 x ( 2 r ) W N / 2 r k A(k)=∑_{r=0}^{N/2-1}x(2r) W_{N/2}^{rk} A(k)=r=0N/21x(2r)WN/2rk
B ( k ) = ∑ r = 0 N / 2 − 1 x ( 2 r + 1 ) W N / 2 r k B(k)=∑_{r=0}^{N/2-1}x(2r+1) W_{N/2}^{rk} B(k)=r=0N/21x(2r+1)WN/2rk
其中, k 取 0 , 1 , … … N / 2 − 1 k取0,1,……N/2-1 k0,1,……N/21,从而
X ( k ) = A ( k ) + W N k B ( k ) X(k)=A(k)+W_N^k B(k) X(k)=A(k)+WNkB(k)
由于 A ( k ) , B ( k ) A(k),B(k) A(k),B(k)都是 N / 2 N/2 N/2点的DFT,X(k)为N点的DFT,这一分治思想还可以进一步做下去。这一方法通常是使用递归实现的,并行优化的难度较高。
为并行化快速傅里叶变换,需要使用非迭代版本,即先预处理每个位置上元素变换后的位置(每个位置分治后的最终位置为其二进制翻转后得到的位置),然后先将所有元素移到变换后的位置之后直接循环合并。
调整完循环顺序之后,第一层循环变量i表示每一层变换的跨度,第二层循环变量j表示每一层变换的第一个起点,第三层循环遍历k则表示实际变换的位置k和k+i。在这里,从第二层开始是没有循环依赖的,即对于不同的j,算法不会对同一块地址进行访问,因为访问的下标k=j(mod i)。
为公平起见,用于对比的串行版本快速傅里叶变换是直接在并行版本上删去编译推导#pragma omp for得到的。这是因为递归版本的快速傅里叶变换通常有较大的函数递归开销。
主函数运行时传入三个参数,第一个参数为.exe文件,第二个参数为并行部分使用的线程数量,第三个参数为傅里叶变换的幂次,因为傅里叶变换算法本身要求长度为2的幂次。
调整线程数与幂次,考虑并行化的加速比。

运行结果

幂次:20

线程数 2 3 4 5 6
串行时间(s) 0.535 0.545 0.475 0.537 0.503
并行时间(s) 0.325 0.296 0.265 0.233 0.241
加速比 1.646 1.841 1.792 2.305 2.087

结果分析

观察结果,发现对于并行程序,虽然线程数增加,但加速比始终保持在2-3之间,猜想可能是以下原因:OpenMP的parallel region结束时,线程之间需要同步,有隐式路障。最好的OpenMP使用场景是线程之间没有很多需要锁保护的共享访问;parallel region应该尽可能大,以抵消OpenMP多线程带来的额外同步开销。

程序源码

#include 
#include
#include
#include
#include 
using namespace std;
typedef long long ll;
typedef double lf;
#define M_PI 3.14159265358979323846
struct Rader : vector<int>
{
    Rader(int n) : vector<int>(1 << int(ceil(log2(n))))
    {
        for (int i = at(0) = 0; i < size(); ++i)
            if (at(i) = at(i >> 1) >> 1, i & 1)
                at(i) += size() >> 1;
    }
};
struct FFT : Rader
{
    vector<complex<lf>> w;
    FFT(int n) : Rader(n), w(size(), polar(1.0, 2 * M_PI / size()))
    {
        w[0] = 1;
        for (int i = 1; i < size(); ++i)
            w[i] *= w[i - 1];
    }
    vector<complex<lf>> pfft1(const vector<complex<lf>> &a) const
    {
        vector<complex<lf>> x(size());
#pragma omp parallel for
        for (int i = 0; i < a.size(); ++i)
            x[at(i)] = a[i];
        for (int i = 1; i < size(); i <<= 1)
#pragma omp parallel for
            for (int j = 0; j < i; ++j)
                for (int k = j; k < size(); k += i << 1)
                {
                    complex<lf> t = w[size() / (i << 1) * j] * x[k + i];
                    x[k + i] = x[k] - t, x[k] += t;
                }
        return x;
    }
    vector<complex<lf>> pfft2(const vector<complex<lf>> &a) const
    {
        vector<complex<lf>> x(size());
#pragma omp parallel
#pragma omp for
        for (int i = 0; i < a.size(); ++i)
            x[at(i)] = a[i];
        for (int i = 1; i < size(); i <<= 1)
#pragma omp for
            for (int j = 0; j < i; ++j)
                for (int k = j; k < size(); k += i << 1)
                {
                    complex<lf> t = w[size() / (i << 1) * j] * x[k + i];
                    x[k + i] = x[k] - t, x[k] += t;
                }
        return x;
    }
    vector<complex<lf>> fft(const vector<complex<lf>> &a) const
    {
        vector<complex<lf>> x(size());
        for (int i = 0; i < a.size(); ++i)
            x[at(i)] = a[i];
        for (int i = 1; i < size(); i <<= 1)
            for (int j = 0; j < i; ++j)
                for (int k = j; k < size(); k += i << 1)
                {
                    complex<lf> t = w[size() / (i << 1) * j] * x[k + i];
                    x[k + i] = x[k] - t, x[k] += t;
                }
        return x;
    }
    vector<ll> ask(const vector<ll> &a, const vector<ll> &b) const
    {
        vector<complex<lf>> xa(a.begin(), a.end()), xb(b.begin(), b.end());
        xa = fft(xa), xb = fft(xb);
        for (int i = 0; i < size(); ++i)
            xa[i] *= xb[i];
        vector<ll> ans(size());
        xa = fft(xa), ans[0] = xa[0].real() / size() + 0.5;
        for (int i = 1; i < size(); ++i)
            ans[i] = xa[size() - i].real() / size() + 0.5;
        return ans;
    }
};
int main(int argc, char **argv)
{
    //if (argc < 3)
        //return cerr << "Error: No Enough parameters (" << argv[0] << "  ).\n", 0;
    //omp_set_num_threads(atoi(argv[1]));
    //FFT fft(1 << atoi(argv[2]));
    FFT fft(1 << 20);
    for(int i=2;i<11;i++){
        omp_set_num_threads(i);
        cout<<"#"<<i<<endl;
        vector<complex<lf>> a(fft.begin(), fft.end());
        double t0 = omp_get_wtime();
        vector<complex<lf>> b = fft.fft(a);
        double t1 = omp_get_wtime();
        cout << "Serial Time: " << t1 - t0 << "s\n";
        vector<complex<lf>> c = fft.pfft1(a);
        double t2 = omp_get_wtime();
        cout << "Parallel Time1: " << t2 - t1 << "s\n";
        vector<complex<lf>> d = fft.pfft2(a);
        double t3 = omp_get_wtime();
        cout << "Parallel Time2: " << t3 - t2<< "s\n";
        if (b != c||c!=d)
            cerr << "Error: Parallel result are not equivalent to Serial result.\n";
    }
}

参考资料

快速傅里叶变换(FFT)超详解
手把手教快速傅立叶变换FFT算法
OpenMP实现并行快速傅里叶变换

你可能感兴趣的:(并行程序设计,并行程序设计,OpenMP)