我正在尝试提出一种评估以下功能的好方法
double foo(std::vector<double> const& x, double c = 0.95)
{
auto N = x.size(); // Small power of 2 such as 512 or 1024
double sum = 0;
for (auto i = 0; i != N; ++i) {
sum += (x[i] * pow(c, double(i)/N));
}
return sum;
}
我对这种幼稚实现的两个主要关注是性能和准确性。因此,我怀疑最微不足道的改进将是反转循环顺序:for (auto i = N-1; i != -1; --i)
(-1环绕,这没关系)。通过先添加较小的术语来提高准确性。
虽然这对准确性有好处,但仍保留了pow
的性能问题。在数字上,pow(c, double(i)/N)
是pow(c, (i-1)/N) * pow(c, 1/N)
。而后者是一个常数。因此,从理论上讲,我们可以将pow
替换为重复乘法。虽然性能不错,但会影响准确性-错误会累积。
我怀疑这里隐藏着一个更好的算法。例如,N是2的幂的事实意味着存在一个乘以sqrt(c)
的中间项x [N / 2]。这暗示了递归的解决方案。
在某种程度上相关的数值观察中,这看起来像是信号与指数的乘积,因此我自然而然地想到:“ FFT,平凡卷积=移位,IFFT”,但是在准确性或性能方面似乎并没有真正的好处。
那么,这是已知解决方案的众所周知的问题吗?
答案 0 :(得分:2)
伊夫的回答启发了我。
最好的方法似乎不是直接计算pow(c, 1.0/N)
,而是间接计算:
cc[0]=c; cc[1]=sqrt(cc[0]), cc[2]=sqrt(cc[1]),... cc[logN] = sqrt(cc[logN-1])
或者以二进制
cc[0]=c, cc[1]=c^0.1, cc[2]=c^0.01, cc[3]=c^0.001, ...
。
现在,如果我们需要x[0b100100] * c^0.100100
,我们可以将其计算为x[0b100100]* c^0.1 * c^0.0001
。我不需要像geza一样预先计算大小为N的表。大小为log(N)的表可能已足够,可以通过反复取平方根来创建它。
[编辑] 正如在另一个答案的注释线程中指出的那样,成对求和在控制错误方面非常有效。它恰好与这个答案完美地结合在一起。
我们首先观察我们的求和
x[0] * c^0.0000000
x[1] * c^0.0000001
x[2] * c^0.0000010
x[3] * c^0.0000011
...
因此,我们运行log(N)迭代。在迭代1中,我们添加了N / 2对x[i]+x[i+1]*c^0.000001
并将结果存储在x[i/2]
中。在迭代2中,我们添加了对x[i]+x[i+1]*c^0.000010
等。与普通的成对求和的主要区别在于,这是每一步的乘加运算。
我们现在看到在每个迭代中,我们都使用相同的乘数pow(c, 2^i/N)
,这意味着我们只需要计算log(N)乘数。由于我们仅进行连续的内存访问,因此它也具有相当高的缓存效率。它还可以轻松实现SIMD并行化,尤其是在您具有FMA指令时。
答案 1 :(得分:2)
任务是多项式求值。具有最少操作计数的单次评估方法是霍纳方案。通常,较低的运算数量会减少浮点噪声的累积。
当示例值c=0.95
接近1
时,任何根都将更接近1
并因此失去准确性。通过
1
与z=1-c^(1/n)
的差异,可以避免这种情况
z = -expm1(log(c)/N).
现在您必须评估多项式
sum of x[i] * (1-z)^i
可以通过仔细修改Horner方案来完成。代替
for(i=N; i-->0; ) {
res = res*(1-z)+x[i]
}
使用
for(i=N; i-->0; ) {
res = (res+x[i])-res*z
}
在数学上是等效的,但是1-z
中的数字丢失发生得尽可能晚,而没有使用像双精度加法这样的复杂方法。
在测试中,这两种与意图相反的方法给出了几乎相同的结果,通过将结果分成c=1, z=0
的值和z
的倍数(如中),可以观察到实质性的改进
double res0 = 0, resz=0;
int i;
for(i=N; i-->0; ) {
/* res0+z*resz = (res0+z*resz)*(1-z)+x[i]; */
resz = resz - res0 -z*resz;
res0 = res0 + x[i];
}
表明这种改进的测试案例是针对
的系数序列f(u) = (1-u/N)^(N-2)*(1-u)
N=1000
的评估结果在何处
c z=1-c^(1/N) f(1-z) diff for 1st proc diff for 3rd proc
0.950000 0.000051291978909 0.000018898570629 1.33289104579937e-17 4.43845264361253e-19
0.951000 0.000050239954368 0.000018510931892 1.23765066121009e-16 -9.24959978401696e-19
0.952000 0.000049189034371 0.000018123700958 1.67678642238461e-17 -5.38712954453735e-19
0.953000 0.000048139216599 0.000017736876972 -2.86635949350855e-17 -2.37169225231204e-19
...
0.994000 0.000006018054217 0.000002217256601 1.31645860662263e-17 1.15619997300212e-19
0.995000 0.000005012529261 0.000001846785028 -4.15668713370839e-17 -3.5363625547867e-20
0.996000 0.000004008013365 0.000001476685973 8.48811716443534e-17 8.470329472543e-22
0.997000 0.000003004504507 0.000001106958687 1.44711343873661e-17 -2.92226366802734e-20
0.998000 0.000002002000667 0.000000737602425 5.6734266807093e-18 -6.56450534122083e-21
0.999000 0.000001000499833 0.000000368616443 -3.72557383333555e-17 1.47701370177469e-20
答案 2 :(得分:1)
如果N
是2
的幂,则可以使用
a^(i+j)/2 = √(a^i.a^j)
,然后从c^N/N.c^0/N
递归细分。借助预排序递归,您可以确保通过增加权重进行累加。
无论如何,sqrt
与pow
的提速可能是微不足道的。
您还可以在仅使用乘积的情况下将递归停止在某个级别并线性继续。
答案 3 :(得分:0)
您可以将重复乘以pow(c, 1./N)
与一些明确的pow
调用混合在一起。即每进行16次迭代,就进行一次实数pow
,否则进行乘法运算。这应该会以很小的精度成本带来巨大的性能优势。
根据c
的变化量,您甚至可以预先计算并使用查找替换所有pow调用,或者仅使用上述方法中所需的调用(=较小的查找表=更好的缓存)。 / p>