最近我正在分析一个程序,其中热点肯定是这个
double d = somevalue();
double d2=d*d;
double c = 1.0/d2 // HOT SPOT
之后不使用值d2,因为我只需要值c。前段时间我读过关于快速反平方根的Carmack方法,显然不是这种情况,但我想知道类似的算法是否可以帮助我计算1 / x ^ 2.
我需要非常准确的精度,我已经检查过我的程序没有使用gcc -ffast-math选项给出正确的结果。 (克++ - 4.5)
答案 0 :(得分:19)
快速平方根等技巧通过牺牲精度来获得性能。 (好吧,大部分都是。)
您确定需要double
精度吗?你可以很容易地牺牲精度:
double d = somevalue();
float c = 1.0f / ((float) d * (float) d);
在这种情况下,1.0f
绝对是强制性的,如果您使用1.0
,则会获得double
精度。
您是否尝试在编译器上启用“草率”数学?在GCC上,您可以使用-ffast-math
,其他编译器也有类似的选项。草率的数学运算可能足以满足您的应用需求。 (编辑:我没有看到生成的程序集有任何差异。)
如果您使用的是GCC,您是否考虑过使用-mrecip
?有一个“倒数估计”函数,它只有大约12位的精度,但速度要快得多。您可以使用Newton-Raphson方法来提高结果的精度。 -mrecip
选项将使编译器自动为您生成倒数估计和Newton-Raphson步骤,但如果您想微调性能 - 精度权衡,您可以自己编写程序集。 (Newton-Raphson快速收敛非常。)(编辑:我无法让GCC生成RCPSS。见下文。)
我发现了一篇博文(source)讨论了你正在经历的确切问题,作者的结论是像Carmack方法这样的技术与RCPSS指令({{1} GCC使用的标志)。
原因为什么划分速度如此之慢是因为处理器通常只有一个除法单元而且通常不会流水线化。因此,您可以在管道中进行一些同时执行的乘法,但在上一个分区完成之前不能发布任何除法。
Carmack的方法:它在具有相互估计操作码的现代处理器上已经过时了。对于倒数,我见过的最好的版本只给出了一点精度 - 与-mrecip
的12位相比没什么。我认为这个技巧对于互惠的平方根很有效,这是巧合;这是一个不太可能重演的巧合。
重新贴标签变量。就编译器而言,RCPSS
和1.0/(x*x)
之间的差异非常小。如果您发现编译器为两个版本生成不同的代码并且优化已打开,即使是最低级别,我会感到惊讶。
使用double x2 = x*x; 1.0/x2
。 pow
库函数是一个完整的怪物。关闭GCC的pow
,图书馆电话费用相当昂贵。启用GCC -ffast-math
后,您获得与-ffast-math
完全相同的pow(x, -2)
汇编代码,因此没有任何好处。
以下是双精度浮点值的反平方的Newton-Raphson近似的示例。
1.0/(x*x)
不幸的是,我的计算机上的static double invsq(double x)
{
double y;
int i;
__asm__ (
"cvtpd2ps %1, %0\n\t"
"rcpss %0, %0\n\t"
"cvtps2pd %0, %0"
: "=x"(y)
: "x"(x));
for (i = 0; i < RECIP_ITER; ++i)
y *= 2 - x * y;
return y * y;
}
基准测试比简单版本RECIP_ITER=1
略慢(~5%)。它的速度更快(2倍速度),零迭代,但是你只能得到12位精度。我不知道12位是否足够你。
我认为这里的一个问题是这个微观优化太小了;在这种规模上,编译器编写者与汇编黑客几乎是平等的。也许如果我们有更大的图景,我们可以看到一种方法来加快速度。
例如,你说1.0/(x*x)
造成了不良的精确度损失;这可能表示您正在使用的算法中存在数值稳定性问题。通过正确选择算法,可以使用-ffast-math
而不是float
来解决许多问题。 (当然,你可能只需要超过24位。我不知道。)
如果你想并行计算其中几个,我怀疑double
方法会发光。
答案 1 :(得分:5)
是的,你当然可以尝试解决问题。让我给你一些一般性的想法,你可以填写详细信息。
首先,让我们看看为什么Carmack的根工作原理:
我们以通常的方式写 x = M ×2 E 。现在回想一下,IEEE浮点数通过偏差存储指数偏移:如果 e 表示指数字段,我们有e =偏差+ E ≥0。重新排列,我们得到< EM>电子 = E - 偏置
现在反平方根: x -1/2 = M -1/2 × 2 - 电子 / 2 。新的指数字段是:
E' =偏置 - 电子 / 2 = 3/2偏置 - E / 2
随着比特摆弄,我们可以通过移位从 e 获得值 e / 2,并且3/2偏差只是一个常数。
此外,尾数 M 存储为1.0 + x , x &lt; 1,我们可以将 M -1/2 近似为1 + x / 2。同样,只有 x 以二进制形式存储的事实意味着我们通过简单的位移来将除法除以2。
现在我们看一下 x -2 :这等于 M -2 ×2 -2 E ,我们正在寻找一个指数字段:
E' =偏置 - 2 电子 = 3偏置 - 2 ë
同样,3个偏差只是一个常数,你可以通过位移从 e 获得2 e 。对于尾数,你可以用1 - 2 x 来近似(1 + x) -2 ,因此问题减少到获得2 x 来自 x 。
请注意,Carmack的魔术浮点小提琴实际上并不能直接计算结果:相反,它会产生非常准确的估计值,它被用作传统迭代计算的起点。但由于估算非常好,因此您只需要进行几轮后续迭代即可获得可接受的结果。
答案 2 :(得分:1)
对于您当前的程序,您已确定了热点 - 很好。作为加速1 / d ^ 2的替代方法,您可以选择更改程序,使其不经常计算1 / d ^ 2。你能把它从内环中提起来吗?对于多少个不同的d值,你计算1 / d ^ 2?您可以预先计算所需的所有值,然后查找结果吗?这对于1 / d ^ 2来说有点麻烦,但如果1 / d ^ 2是一些较大的代码块的一部分,那么应用这个技巧可能是值得的。你说如果你降低精度,你就得不到足够好的答案。有没有什么方法可以改写代码,这可能会提供更好的行为?数值分析是微妙的,可能值得尝试一些事情,看看发生了什么。
理想情况下,当然,您会发现一些经过多年研究的优化例程 - 您可以链接到lapack或linpack中的任何内容吗?