FFTW与Matlab FFT比较

38

我曾在Matlab中心发布了这个问题,但没有得到任何回应,所以我觉得我应该在这里重新发布。

最近我在Matlab中编写了一个简单的程序,使用了一个for循环中的FFT;FFT占据了计算的主导地位。出于实验目的,我使用FFT库FFTW 3.3在mex中编写了相同的程序。结果是,对于非常大的数组(大约是两倍以上),Matlab程序比mex程序运行得更快。mex程序使用了智能策略,并执行了相同的FFT计算。我也知道Matlab使用FFTW,但是他们的版本是否稍微优化了一些呢?我甚至使用了FFT_EXHAUSTIVE标志,对于大型数组,它仍然比MATLAB版本慢两倍左右。此外,我确保所使用的Matlab是单线程的,并使用“-singleCompThread”标志,而我使用的mex文件没有处于调试模式。只是好奇这是否是真的 - 或者Matlab正在使用一些我不知道的优化。谢谢。

下面是mex部分:

void class_cg_toeplitz::analysis() {
// This method computes CG iterations using FFTs
    // Check for wisdom
    if(fftw_import_wisdom_from_filename("cd.wis") == 0) {
        mexPrintf("wisdom not loaded.\n");
    } else {
        mexPrintf("wisdom loaded.\n");
    }

    // Set FFTW Plan - use interleaved FFTW
    fftw_plan plan_forward_d_buffer;    
    fftw_plan plan_forward_A_vec;       
    fftw_plan plan_backward_Ad_buffer;
    fftw_complex *A_vec_fft;
    fftw_complex *d_buffer_fft;
    A_vec_fft = fftw_alloc_complex(n);
    d_buffer_fft = fftw_alloc_complex(n);

    // CREATE MASTER PLAN - Do this on an empty vector as creating a plane 
    // with FFTW_MEASURE will erase the contents; 
    // Use d_buffer
    // This is somewhat dangerous because Ad_buffer is a vector; but it does not
    // get resized so &Ad_buffer[0] should work
    plan_forward_d_buffer = fftw_plan_dft_r2c_1d(d_buffer.size(),&d_buffer[0],d_buffer_fft,FFTW_EXHAUSTIVE);
    plan_forward_A_vec = fftw_plan_dft_r2c_1d(A_vec.height,A_vec.value,A_vec_fft,FFTW_WISDOM_ONLY);
    // A_vec_fft.*d_buffer_fft will overwrite d_buffer_fft
    plan_backward_Ad_buffer = fftw_plan_dft_c2r_1d(Ad_buffer.size(),d_buffer_fft,&Ad_buffer[0],FFTW_EXHAUSTIVE);

    // Get A_vec_fft
    fftw_execute(plan_forward_A_vec);

    // Find initial direction - this is the initial residual
    for (int i=0;i<n;i++) {
        d_buffer[i] = b.value[i];
        r_buffer[i] = b.value[i];
    }    

    // Start CG iterations
    norm_ro = norm(r_buffer);
    double fft_reduction = (double)Ad_buffer.size(); // Must divide by size of vector because inverse FFT does not do this
    while (norm(r_buffer)/norm_ro > relativeresidual_cutoff) {        
        // Find Ad - use fft
        fftw_execute(plan_forward_d_buffer);    
        // Get A_vec_fft.*fft(d) - A_vec_fft is only real, but d_buffer_fft
        // has complex elements; Overwrite d_buffer_fft        
        for (int i=0;i<n;i++) {
            d_buffer_fft[i][0] = d_buffer_fft[i][0]*A_vec_fft[i][0]/fft_reduction;
            d_buffer_fft[i][1] = d_buffer_fft[i][1]*A_vec_fft[i][0]/fft_reduction;
        }        
        fftw_execute(plan_backward_Ad_buffer); 

        // Calculate r'*r
        rtr_buffer = 0;
        for (int i=0;i<n;i++) {
            rtr_buffer = rtr_buffer + r_buffer[i]*r_buffer[i];
        }    

        // Calculate alpha
        alpha = 0;
        for (int i=0;i<n;i++) {
            alpha = alpha + d_buffer[i]*Ad_buffer[i];
        }    
        alpha = rtr_buffer/alpha;

        // Calculate new x
        for (int i=0;i<n;i++) {
            x[i] = x[i] + alpha*d_buffer[i];
        }   

        // Calculate new residual
        for (int i=0;i<n;i++) {
            r_buffer[i] = r_buffer[i] - alpha*Ad_buffer[i];
        }   

        // Calculate beta
        beta = 0;
        for (int i=0;i<n;i++) {
            beta = beta + r_buffer[i]*r_buffer[i];
        }  
        beta = beta/rtr_buffer;

        // Calculate new direction vector
        for (int i=0;i<n;i++) {
            d_buffer[i] = r_buffer[i] + beta*d_buffer[i];
        }  

        *total_counter = *total_counter+1;
        if(*total_counter >= iteration_cutoff) {
            // Set total_counter to -1, this indicates failure
            *total_counter = -1;
            break;
        }
    }

    // Store Wisdom
    fftw_export_wisdom_to_filename("cd.wis");

    // Free fft alloc'd memory and plans
    fftw_destroy_plan(plan_forward_d_buffer);
    fftw_destroy_plan(plan_forward_A_vec);
    fftw_destroy_plan(plan_backward_Ad_buffer);
    fftw_free(A_vec_fft);
    fftw_free(d_buffer_fft);
};

以下是 Matlab 代码:

% Take FFT of A_vec.
A_vec_fft = fft(A_vec); % Take fft once

% Find initial direction - this is the initial residual 
x = zeros(n,1); % search direction
r = zeros(n,1); % residual
d = zeros(n+(n-2),1); % search direction; pad to allow FFT
for i = 1:n
    d(i) = b(i); 
    r(i) = b(i); 
end

% Enter CG iterations
total_counter = 0;
rtr_buffer = 0;
alpha = 0;
beta = 0;
Ad_buffer = zeros(n+(n-2),1); % This holds the product of A*d - calculate this once per iteration and using FFT; only 1:n is used
norm_ro = norm(r);

while(norm(r)/norm_ro > 10^-6)
    % Find Ad - use fft
    Ad_buffer = ifft(A_vec_fft.*fft(d)); 

    % Calculate rtr_buffer
    rtr_buffer = r'*r;

    % Calculate alpha    
    alpha = rtr_buffer/(d(1:n)'*Ad_buffer(1:n));

    % Calculate new x
    x = x + alpha*d(1:n);

    % Calculate new residual
    r = r - alpha*Ad_buffer(1:n);

    % Calculate beta
    beta = r'*r/(rtr_buffer);

    % Calculate new direction vector
    d(1:n) = r + beta*d(1:n);      

    % Update counter
    total_counter = total_counter+1; 
end

就时间而言,当N = 50000且b = 1:n时,使用mex需要约10.5秒,使用matlab需要约4.4秒。我正在使用R2011b版本。谢谢。


你的数据有哪些维度,以及绝对时间是什么? - us2012
它们都是原地FFT吗? - Andy
1
有趣的是,我刚刚查了一下:Matlab也有一个fftw命令,可以控制内部用于fftw库的优化参数(->帮助fftw)。使用此命令,您还可以获取Matlab用于计算的智能数据库。当您将Matlab的智能数据库输入到C++程序中,或者反过来时,您会得到什么样的结果,这将是非常有趣的... - andrsmllr
在Matlab的bin/<PLATFORM>目录下,您可以找到名为'fftw.spec'的文件,该文件指定了不同CPU的不同库-因此我认为这些库是经过特别优化的。 - durasm
这可能是微不足道且无关紧要的,但我注意到在.mex代码中有2个对fftw_execute()的调用;但在Matlab中只有1个。我想我可能错过了一些明显的东西,但我想评论一下。 - Dan Nissenbaum
显示剩余10条评论
4个回答

15
我并不知道 MATLAB FFT 实现的具体细节,所以这里只能提出一些观察结果而非确定的答案:
  • 根据您提供的代码,我可以看到速度差异有两种解释:
    • FFT 优化级别的不同导致了速度差异
    • MATLAB 中的 while 循环执行次数显著较少

假设您已经检查了第二个问题,并且迭代次数是可比较的。(如果不可比较,则很可能与精度问题有关,需要进一步调查。)

现在,就 FFT 速度进行比较:

  • 是的,理论上 FFTW 比其他高级 FFT 实现更快,但只有在您将苹果与苹果进行比较时才相关:在这里,您正在比较更低层次的实现,即在汇编级别上,其中选择算法不仅涉及实际优化,还涉及针对特定处理器进行软件开发的技能差异。
  • 我多年来已经在许多处理器上优化或审查了汇编中的 FFT(我曾在基准测试行业工作),出色的算法只是故事的一部分。你必须考虑与你编码的架构非常相关的因素(考虑延迟,指令调度,寄存器使用的优化,内存中数据的排列,考虑分支的取/不取延迟等),这些因素会产生与选择算法同样重要的差异。
  • 在 N = 500,000 的情况下,我们还谈论大型内存缓冲区:这是更多优化的另一个方面,可以很快变得非常特定于您运行代码的平台:你如何成功地避免高速缓存未命中不仅由算法所决定,更由软件开发人员使用什么优化来有效地将数据带入和带出内存。
  • 尽管我不了解 MATLAB FFT 实现的详细信息,但我相信一支 DSP 工程师团队一直在进行其优化,因为它对于许多设计至关重要。这可能意味着 MATLAB 具有正确的开发人员组合,以制作出更快的 FFT。

1
@jucestain 你说的每一点都指向同一个结论:83-85与85之间的差异意味着仅仅是FFT性能的不同,你的90%与84.99%的分析数据也是如此。MATLAB的实现更加优化,这在像这样的算法中是合理的,因为每个阶段都有很多优化的机会。我不认为这是“底层”的技巧,而只是花费了更多时间来创建一个比你使用的MEX对应物更高级别优化的FFT实现。我认为你的mex实现没有遗漏任何东西。 - Lolo
@jucestain 正如Lolo所说,"引擎盖下"没有任何事情发生,Matlab有一个更好的优化实现来自MKL,请查看我的答案,它回答了你的问题... - reverse_engineer
情节渐渐复杂起来:http://software.intel.com/en-us/articles/intel-mkl-main-libraries-contain-fftw3-interfaces。 - JustinBlaber
1
@jucestain 是的,我非常确定,星期一会再确认一下(我办公室只有Matlab)...而你提供的英特尔文章只是说明英特尔MKL支持FFTW接口,但底层实现是英特尔特定的,他们非常了解自己的处理器,所以能够高效地进行优化。比FFTW开发人员更好。我真的认为这解释了性能差异。 - reverse_engineer
只是提醒一下,我本来会授予全部悬赏的,但这个答案仍然只是猜测而已... - JustinBlaber
显示剩余3条评论

10
这是由于低级别和架构特定优化所带来的经典性能提升。
Matlab使用Intel MKL(数学核心库)二进制文件(mkl.dll)中的FFT。这些例程由Intel针对Intel处理器进行了优化(在汇编级别)。即使在AMD上,它似乎也能提供不错的性能提升。
FFTW似乎是一个普通的C库,没有被优化得那么好。因此,使用MKL可以获得性能提升。

4
MATLAB自带开源FFTW库的构建版本,支持多线程和SSE/AVX向量化指令。调用version('-fftw')将显示FFTW-3.3.3-sse2-avx。在MATLAB bin文件夹中有两个共享库,它们导出了FFTW API接口:libmwfftw3.dlllibmwfftw3f.dll(除了第三个库libmwmfl_fft.dll,该库基于前两个库,用于抽象使用FFTW计划)。因此,尽管MATLAB使用Intel MKL作为优化的BLAS/LAPACK实现,但据我所知,它并没有从MKL中调用FFTW接口。 - Amro
@Amro 感谢您的澄清!顺便问一下,您是如何发现这两个二进制文件导出了 FFTW API 接口的?您知道这两个二进制文件之间有什么区别吗?在我的 R2010a 中,我只有一个 libmwfftw.dll 库... - reverse_engineer
1
我只是使用Dependency Walker来获取任何DLL导出函数的列表(您将看到熟悉的函数,如fftw_plan_dft_1dfftw_execute等)。第一个DLL对应于FFTW的双精度版本,第二个DLL对应于单精度版本(我有最新的MATLAB R2014a)。我忘了说还有另外两个DLL文件实现了使用MPI的分布式内存并行版本的FFTW(查找libmwfftw3_mpi.dlllibmwfftw3f_mpi.dll)。 - Amro
2
如果您有PCT工具箱,fft也可以在GPU上运行,这是使用cuFFT库实现的(查找cufft*.dll文件)。 - Amro
@Amro 谢谢你的信息。 - reverse_engineer

3

编辑:@wakjah对这个答案的回复是准确的:FFTW通过其Guru接口支持拆分实部和虚部存储。因此,我的关于hack的说法是不准确的,但如果没有使用FFTW的Guru接口,则仍然适用-这是默认情况下的情况,所以仍需谨慎!

首先,很抱歉一年后才回复。我并不确定你所看到的速度提升来自MKL或其他优化。FFTW和Matlab之间有一个非常根本的不同之处,那就是如何在内存中存储复杂数据。

在Matlab中,复杂向量X的实部和虚部是分开的数组Xre[i]和Xim[i](在线性内存中,单独操作它们时效率高)。

在FFTW中,默认情况下,实部和虚部是交错存储的,即double[2],即X[i][0]为实部,X[i][1]为虚部。

因此,在mex文件中使用FFTW库时,不能直接使用Matlab数组,而必须先分配新的内存,然后将输入从Matlab打包成FFTW格式,然后再将输出从FFTW解包成Matlab格式。也就是说:

X = (fftw_complex*) fftw_malloc(sizeof(fftw_complex) * N);
Y = (fftw_complex*) fftw_malloc(sizeof(fftw_complex) * N);

那么

for (size_t i=0; i<N; ++i) {
    X[i][0] = Xre[i];
    X[i][1] = Xim[i];
}

那么

for (size_t i=0; i<N; ++i) {
    Yre[i] = Y[i][0];
    Yim[i] = Y[i][1];
}

因此,这需要2倍的内存分配 + 4倍的读取 + 4倍的写入 - 都是大小为N。在处理大问题时,这确实对速度产生了影响。
我有一个直觉,Mathworks可能已经修改了FFTW3代码,以使其能够直接读取Matlab格式的输入向量,从而避免了上述所有问题。
在这种情况下,只能分配X并使用X来运行FFT in-place(作为fftw_plan_*(N, X, X, ...)而不是 fftw_plan_*(N, X, Y, ...)),因为它将被复制到Yre和Yim Matlab向量中,除非应用程序需要/受益于保持X和Y分开。
编辑:当运行Matlab的fft2()和基于fftw3库的代码时,实时查看内存消耗时,显示Matlab仅分配一个额外的复杂数组(输出),而我的代码需要两个这样的数组(*fftw_complex缓冲区加上Matlab输出)。不能在Matlab和fftw格式之间进行原位转换,因为Matlab的实数和虚数数组在内存中不连续。这表明Mathworks修改了fftw3库以使用Matlab格式读取/写入数据。
多次调用的另一个优化是要持久地分配(使用mexMakeMemoryPersistent())。我不确定Matlab实现是否也这样做。
干杯。
附言:作为副产品,Matlab的复杂数据存储格式更适合单独操作实数或虚数向量。在FFTW的格式中,您必须进行++2次内存读取。

1
除了FFTW Guru Interface支持分割的实数和复数数组 - 即与MATLAB格式相同 - 不需要任何黑客手段。 - wakjah
@wakjah,我认错了,+1并感谢!我编辑了我的答案以反映您的回复。 - Normadize

3
我在MathWorks网站[1]上找到了以下评论:
关于2的大幂次方:对于FFT维度为2的幂次方,介于2^14和2^22之间,MATLAB软件使用其内部数据库中的特殊预加载信息来优化FFT计算。当FFT的维数是2的幂次方时,不进行调整,除非您使用命令fftw('wisdom', [])清除数据库。
尽管它与2的幂有关,但它可能暗示MATLAB在使用FFTW处理某些(大)数组大小时采用自己的“特殊智慧”。考虑:2^16 = 65536。
[1] R2013b文档可从http://www.mathworks.de/de/help/matlab/ref/fftw.html获取(于2013年10月29日访问)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接