安全快速的FFT

时间:2012-04-12 10:13:14

标签: c++ performance c++11 fft

受到Herb Sutter引人注目的讲座Not your father's 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;
            }
        }
}

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

据我了解,C中用于索引xs[i]的传统vector语法不进行边界检查,因此不是内存安全的,可能是内存错误的来源,例如非 - 断言腐败和内存访问违规。所以我改为使用了xs.at(i)

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

1 个答案:

答案 0 :(得分:14)

我认为你在使用at()方面过于“安全”。在大多数情况下,使用的索引可以通过for循环中的容器大小进行约束来轻易验证。

e.g。

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

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

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

int m=zs.size()/2;
pi * sign
2*j

zs.at(i + j)计算两次。

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

谈论第二次猜测优化器:我确信对.size()的调用将被内联为至少直接调用内部成员变量 - 但是考虑到你调用它的次数我是d还试验为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&lt; j check可以归结为std :: swap。这更具惯用性,至少在性能上 - 在最坏的情况下应该内联到与编写的相同的代码。确实,当我这样做时,我发现性能没有变化。在其他情况下,如果移动构造函数可用,则可以提高性能。