作为基准测试,我决定尝试用各种语言编写相同的简单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专家,所以我想要求对这段代码进行改进,使其更具惯用性或效率?
答案 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。这更具惯用性,至少在性能上 - 在最坏的情况下应该内联到与编写的相同的代码。确实,当我这样做时,我发现性能没有变化。在其他情况下,如果移动构造函数可用,则可以提高性能。