用指数函数求乘法

时间:2018-07-10 09:32:15

标签: c++ numerical-methods

我正在尝试提出一种评估以下功能的好方法

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”,但是在准确性或性能方面似乎并没有真正的好处。

那么,这是已知解决方案的众所周知的问题吗?

4 个答案:

答案 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并因此失去准确性。通过

直接计算与1z=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)

如果N2的幂,则可以使用

以几何方式替换幂的评估
a^(i+j)/2 = √(a^i.a^j)

,然后从c^N/N.c^0/N递归细分。借助预排序递归,您可以确保通过增加权重进行累加。

无论如何,sqrtpow的提速可能是微不足道的。

您还可以在仅使用乘积的情况下将递归停止在某个级别并线性继续。

答案 3 :(得分:0)

您可以将重复乘以pow(c, 1./N)与一些明确的pow调用混合在一起。即每进行16次迭代,就进行一次实数pow,否则进行乘法运算。这应该会以很小的精度成本带来巨大的性能优势。

根据c的变化量,您甚至可以预先计算并使用查找替换所有pow调用,或者仅使用上述方法中所需的调用(=较小的查找表=更好的缓存)。 / p>