C++ 用指数函数求乘法
我试图想出一个好的方法来评估下面的函数C++ 用指数函数求乘法,c++,numerical-methods,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));
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;
}
我对这种幼稚实现的两个主要关注点是性能和准确性。因此,我怀疑最微不足道的改进是颠倒循环顺序:对于自动I=N-1;我!=-1; -我把-1包起来,这没关系。这通过首先添加较小的术语来提高准确性
虽然这有助于提高精度,但它保留了pow的性能问题。在数值上,powc,doublei/N是powc,i-1/N*powc,1/N。后者是一个常数。所以理论上我们可以用重复乘法来代替pow。虽然对性能有好处,但这会损害准确性-错误会累积
我怀疑这里隐藏着一个更好的算法。例如,N是2的幂这一事实意味着有一个中间项x[N/2]乘以sqrtc。这暗示了一个递归解决方案
在某种程度上相关的数值观察中,这看起来像是信号与指数相乘,所以我自然认为:FFT,琐碎卷积=移位,IFFT,但这似乎在精度或性能方面没有真正的好处
那么,这是一个已知解决方案的众所周知的问题吗 如果N是2的幂,可以使用几何平均值替换幂的计算
a^(i+j)/2 = √(a^i.a^j)
并从c^N/N.c^0/N递归细分。使用前序递归,可以确保通过增加权重进行累加
无论如何,sqrt对pow的加速可能是微不足道的
您还可以在某个级别停止递归,然后线性继续,仅使用乘积。如果N是2的幂,您可以使用几何平均值替换幂的计算
a^(i+j)/2 = √(a^i.a^j)
并从c^N/N.c^0/N递归细分。使用前序递归,可以确保通过增加权重进行累加
无论如何,sqrt对pow的加速可能是微不足道的
您还可以在某个级别停止递归,然后线性继续,只使用乘积。您可以将powc,1./N的重复乘法与一些显式pow调用混合使用。也就是说,大约每第16次迭代做一个真实的pow,然后继续乘法。这将以可忽略不计的精度成本产生巨大的性能优势
根据c的变化程度,您甚至可以预先计算所有pow调用,并用查找替换它们,或者仅使用上述方法所需的调用=较小的查找表=更好的缓存。您可以将powc的重复乘法,1./N与一些显式pow调用混合使用。也就是说,大约每第16次迭代做一个真实的pow,然后继续乘法。这将以可忽略不计的精度成本产生巨大的性能优势
根据c的变化程度,您甚至可以预计算并使用查找替换所有pow调用,或者仅使用上述方法所需的调用=较小的查找表=更好的缓存。Yves的回答启发了我 似乎最好的方法不是直接计算powc,1.0/N,而是间接计算: cc[0]=c;cc[1]=sqrtcc[0],cc[2]=sqrtcc[1],。。。cc[logN]=sqrtcc[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大小的桌子。一个logN大小的表可能就足够了,可以通过反复求平方根来创建它 [编辑] 正如在另一个答案的评论帖子中指出的,两两相加在控制错误方面非常有效。它恰好与这个答案结合得非常好 我们从观察我们求和开始
x[0] * c^0.0000000
x[1] * c^0.0000001
x[2] * c^0.0000010
x[3] * c^0.0000011
...
所以,我们运行logN迭代。在迭代1中,我们添加N/2对x[i]+x[i+1]*c^0.000001,并将结果存储在x[i/2]中。在迭代2中,我们添加对x[i]+x[i+1]*c^0.000010,等等。与正常两两求和的主要区别在于,这是每一步的乘法和加法
现在我们看到,在每次迭代中,我们使用相同的乘数powc,2^i/N,这意味着我们只需要计算logN乘数。它的缓存效率也很高,因为我们只进行连续内存访问。它还允许简单的SIMD并行化,特别是当您有FMA指令时。Yves的回答启发了我 似乎最好的方法不是直接计算powc,1.0/N,而是间接计算: cc[0]=c;cc[1]=sqrtcc[0],cc[2]=sqrtcc[1],。。。cc[logN]=sqrtcc[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大小的桌子。一个logN大小的表可能就足够了,可以通过反复求平方根来创建它 [编辑] 正如在anoth的评论帖子中指出的 呃,答案是,两两相加在控制错误方面非常有效。它恰好与这个答案结合得非常好 我们从观察我们求和开始
x[0] * c^0.0000000
x[1] * c^0.0000001
x[2] * c^0.0000010
x[3] * c^0.0000011
...
所以,我们运行logN迭代。在迭代1中,我们添加N/2对x[i]+x[i+1]*c^0.000001,并将结果存储在x[i/2]中。在迭代2中,我们添加对x[i]+x[i+1]*c^0.000010,等等。与正常两两求和的主要区别在于,这是每一步的乘法和加法
现在我们看到,在每次迭代中,我们使用相同的乘数powc,2^i/N,这意味着我们只需要计算logN乘数。它的缓存效率也很高,因为我们只进行连续内存访问。它还允许简单的SIMD并行化,特别是当您有FMA指令时。该任务是多项式计算。操作次数最少的单一评估方法是Horner方案。通常,较低的操作计数将减少浮点噪声的累积 由于示例值c=0.95接近1,因此任何根都将更接近1,从而失去准确性。通过直接计算差为1,z=1-c^1/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的倍数,可以观察到显著的改善,如中所示
显示这种改进的测试用例是针对序列的系数
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
这项任务是多项式评估。操作次数最少的单一评估方法是Horner方案。通常,较低的操作计数将减少浮点噪声的累积 由于示例值c=0.95接近1,因此任何根都将更接近1,从而失去准确性。通过直接计算差为1,z=1-c^1/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的倍数,可以观察到显著的改善,如中所示
显示这种改进的测试用例是针对序列的系数
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
如果c变化不大,您可以为powc创建表,…顺便说一句,这不是卷积,只是一个简单的点积,所以我认为FFT没有任何用处here@geza:同意。这是一个乘法运算;FFT将把它变成一个微不足道的卷积。但是FFT和IFFT的价格似乎很高。只有在需要卷积时才应使用FFT,以使算法从^2变为logN。但是你的算法已经是最好的了,所以你不能通过切换算法来做得更好。你可以优化它,所以常数因子会更小。c的范围是什么?如果c变化不大,你可以为powc创建表格,…顺便说一句,这不是卷积,只是一个简单的点积,所以我认为FFT没有任何用处here@geza:同意。这是一个乘法运算;FFT将把它变成一个微不足道的卷积。但是FFT和IFFT的价格似乎很高。只有在需要卷积时才应使用FFT,以使算法从^2变为logN。但是你的算法已经是最好的了,所以你不能通过切换算法来做得更好。你可以优化它,所以常数因子会更小。c的范围是什么?更合理的选择是每8次或16次迭代一次;10不除以1024。更合理的选择是每第8次或第16次迭代;10不除以1024。最小运算计数:对于这些功率的评估,就误差累积而言,霍纳方案达到了最差的运算计数,ON。仅使用OLog N运算设计解决方案是可能的。对于具有一般系数序列的多项式求值,由于每个N系数都必须包含在过程中,因此不会比ON更好。因此,N次乘法和N次加法相当低。多点评估是另一个问题,其中傅里叶或基于插值的技术比每个采样点获得更好的复杂性。不,我不是说时间复杂性,我是说单点评估中的误差累积。[顺便说一下,方法
我指的是保持时间复杂性。]expm1似乎是避免精度损失的关键步骤。@YvesDaoust:你在考虑成对加法吗?我想这可能对我的回答更有好处。我首先为所有偶数I添加x[I]+c^1/N*x[I+1],将结果存储在x[I]中。然后加上x[i]+c^2/N*x[i+2],存储在x[i]中,以此类推。运算次数最少:对于这些功率的评估,就误差累积而言,霍纳方案的运算次数最差,为。仅使用OLog N运算设计解决方案是可能的。对于具有一般系数序列的多项式求值,由于每个N系数都必须包含在过程中,因此不会比ON更好。因此,N次乘法和N次加法相当低。多点评估是另一个问题,其中傅里叶或基于插值的技术比每个采样点获得更好的复杂性。不,我不是说时间复杂性,我是说单点评估中的误差累积。[顺便说一句,我所指的方法保持时间复杂性。]expm1似乎是避免精度损失的关键步骤。@YvesDaoust:你在考虑两两相加吗?我想这可能对我的回答更有好处。我首先为所有偶数I添加x[I]+c^1/N*x[I+1],将结果存储在x[I]中。然后添加x[i]+c^2/N*x[i+2],存储在x[i]中,等等。