安全快速的FFT

Jon*_*rop 14 c++ performance fft c++11

灵感来自Herb Sutter引人注目的讲座不是你父亲的C++,我决定再使用微软的Visual Studio 2010来看看最新版本的C++.我特别感兴趣的是Herb断言C++"安全而快速",因为我写了很多性能关键代码.

作为基准,我决定尝试用各种语言编写相同的简单FFT算法.

我想出了以下使用内置complex类型和vector集合的C++ 11代码:

#include <complex>
#include <vector>

using namespace std;

// Must provide type or MSVC++ barfs with "ambiguous call to overloaded function"
double pi = 4 * atan(1.0);

void fft(int sign, vector<complex<double>> &zs) {
    unsigned int j=0;
    // Warning about signed vs unsigned comparison
    for(unsigned int i=0; i<zs.size()-1; ++i) {
        if (i < j) {
            auto t = zs.at(i);
            zs.at(i) = zs.at(j);
            zs.at(j) = t;
        }
        int m=zs.size()/2;
        j^=m;
        while ((j & m) == 0) { m/=2; j^=m; }
    }
    for(unsigned int j=1; j<zs.size(); j*=2)
        for(unsigned int m=0; m<j; ++m) {
            auto t = pi * sign * m / j;
            auto w = complex<double>(cos(t), sin(t));
            for(unsigned int i = m; i<zs.size(); i+=2*j) {
                complex<double> zi = zs.at(i), t = w * zs.at(i + j);
                zs.at(i) = zi + t;
                zs.at(i + j) = zi - t;
            }
        }
}
Run Code Online (Sandbox Code Playgroud)

请注意,此函数仅适用于 - n元素向量,其中n是2的整数幂.任何寻找适合任何人的快速FFT代码的人都n应该关注FFTW.

据我了解,xs[i]C中用于索引a 的传统语法vector不进行边界检查,因此不具有内存安全性,并且可能是内存错误的来源,例如非确定性损坏和内存访问冲突.所以我用了xs.at(i).

现在,我希望这段代码"安全快速",但我不是C++ 11专家,所以我想要求对这段代码进行改进,以使其更具惯用性或效率?

phi*_*red 14

我认为你在使用at()方面过于"安全".在大多数情况下,使用的索引可以简单地验证为受for循环中容器的大小限制.

例如

  for(unsigned int i=0; i<zs.size()-1; ++i) { 
    ...
    auto t = zs.at(i); 
Run Code Online (Sandbox Code Playgroud)

在()s中我唯一留下的是(i + j)s.它们是否总是受到约束并不是很明显(尽管如果我真的不确定我可能会手动检查 - 但我不熟悉FFT足以在这种情况下发表意见).

每次循环迭代都会重复一些固定计算:

int m=zs.size()/2;
pi * sign
2*j
Run Code Online (Sandbox Code Playgroud)

并且zs.at(i + j)被计算两次.

优化器可能会捕获这些 - 但如果您将此视为性能关键,并且您让计时器测试它,我会将它们从循环中提升(或者,在zs.at(i + j)的情况下),只需参考第一次使用),看看是否会影响计时器.

谈论第二次猜测优化器:我确信对.size()的调用将被内联,至少是对内部成员变量的直接调用 - 但是考虑到你调用它多少次,我也会进行实验为zs.size()和zs.size() - 1引入局部变量.他们更有可能以这种方式进入寄存器.

我不知道所有这些对你的总运行时间有多大差异(如果有的话) - 其中一些可能已被优化器捕获,并且与所涉及的计算相比差异可能很小 - 但值得一试射击.

至于惯用语我的唯一注释,实际上,size()返回一个std :: size_t(通常是unsigned int的typedef - 但是使用该类型更加惯用).如果你确实想要使用auto但是避免警告你可以尝试将ul后缀添加到0 - 但不确定我会说这是惯用的.我想你在这里不使用迭代器已经不那么惯用了,但我明白为什么你不能这样做(很容易).

更新

我尝试了所有的建议,他们都有可测量的性能改进 - 除了i + j和2*j预处理 - 它们实际上导致了轻微的减速!我认为他们要么阻止编译器优化,要么阻止它使用寄存器来处理某些事情.

总的来说,我的性能提高了10%.改善这些建议.我怀疑如果第二个循环块被重构一点以避免跳转,我可能会有更多 - 并且这样做启用SSE2指令集可能会给出一个显着的提升(我确实按原样尝试并看到轻微的减速).

我认为重构,以及使用像MKL这样的cos和sin调用之类的东西应该会给出更大,更不易碎的改进.这些东西都不依赖于语言(我知道这最初是与F#实现进行比较).

更新2

我忘了提到预先计算zs.size()确实有所作为.

更新3

也忘了说(直到@xeo在对OP的评论中提醒)i <j check之后的块可以归结为std :: swap.这更具惯用性,至少在性能上 - 在最坏的情况下应该内联到与编写的相同的代码.确实,当我这样做时,我发现性能没有变化.在其他情况下,如果移动构造函数可用,则可以提高性能.