反双曲函数asinh()
与自然对数密切相关。我正在尝试从C99标准数学函数asinh()
确定最准确的计算log1p()
的方法。为了便于实验,我现在仅限于IEEE-754单精度计算,即我正在查看asinhf()
和log1pf()
。我打算重复使用完全相同的算法进行双精度计算,即asinh()
和log1p()
。
我的主要目标是最小化ulp错误,次要目标是最小化错误舍入结果的数量,在改进代码最多比下面发布的版本最低的约束下。任何提高准确度的改进,比如说0.2 ulp,都是值得欢迎的。添加几个FMA(融合乘法 - 加法)会很好,另一方面我希望有人可以找到一个采用快速rsqrtf()
(倒数平方根)的解决方案。
生成的C99代码应该适用于矢量化,可能是通过一些简单的直接转换。所有中间计算必须以函数参数和结果的精度发生,因为任何切换到更高精度可能会产生严重的负面性能影响。代码必须在IEEE-754非正常支持和FTZ(刷新到零)模式下正常工作。
到目前为止,我已经确定了以下两个候选实现。请注意,可以通过单次调用log1pf()
轻松地将代码转换为无分支矢量化版本,但在此阶段我还没有这样做以避免不必要的混淆。
/* for a >= 0, asinh(a) = log (a + sqrt (a*a+1))
= log1p (a + (sqrt (a*a+1) - 1))
= log1p (a + sqrt1pm1 (a*a))
= log1p (a + (a*a / (1 + sqrt(a*a + 1))))
= log1p (a + a * (a / (1 + sqrt(a*a + 1))))
= log1p (fma (a / (1 + sqrt(a*a + 1)), a, a)
= log1p (fma (1 / (1/a + sqrt(1/a*a + 1)), a, a)
*/
float my_asinhf (float a)
{
float fa, t;
fa = fabsf (a);
#if !USE_RECIPROCAL
if (fa >= 0x1.0p64f) { // prevent overflow in intermediate computation
t = log1pf (fa) + 0x1.62e430p-1f; // log(2)
} else {
t = fmaf (fa / (1.0f + sqrtf (fmaf (fa, fa, 1.0f))), fa, fa);
t = log1pf (t);
}
#else // USE_RECIPROCAL
if (fa > 0x1.0p126f) { // prevent underflow in intermediate computation
t = log1pf (fa) + 0x1.62e430p-1f; // log(2)
} else {
t = 1.0f / fa;
t = fmaf (1.0f / (t + sqrtf (fmaf (t, t, 1.0f))), fa, fa);
t = log1pf (t);
}
#endif // USE_RECIPROCAL
return copysignf (t, a); // restore sign
}
使用特定的log1pf()
实现精确到< 0.6 ulps,我在所有2个 32 可能的IEEE-754单精度输入上进行详尽测试时,我观察到以下误差统计。当USE_RECIPROCAL = 0
时,最大错误为1.49486 ulp,并且有353,587,822个错误的舍入结果。使用USE_RECIPROCAL = 1
时,最大错误为1.50805 ulp,并且只有77,569,390个错误的舍入结果。
就性能而言,如果倒数和完全除法花费大致相同的时间量,则变体USE_RECIPROCAL = 0
会更快,但如果可以获得非常快速的互惠支持,变量USE_RECIPROCAL = 1
可能会更快。
答案可以假设所有基本算术,包括FMA(融合乘法 - 加法)都是根据IEEE-754舍入到最接近或偶数模式正确舍入的。 另外,更快,几乎正确舍入的版本的互惠和rsqrtf()
可能可用,其中“几乎正确舍入”意味着最大的ulp错误将受到限制比如0.53 ulps和绝大多数结果,比如> 95%,正确圆润。定向舍入的基本算术可以,无需额外的性能成本。
答案 0 :(得分:1)
首先,您可能想要了解log1pf
函数的准确性和速度:这些在libms之间可能有所不同(我发现OS X数学函数要快,glibc函数要快较慢,但通常是正确的圆形)。
Openlibm,基于BSD libm,而后者又基于Sun的fdlibm,按范围使用多种方法,但主要位是the relation:
t = x*x;
w = log1pf(fabsf(x)+t/(one+sqrtf(one+t)));
您可能还想尝试使用-fno-math-errno
选项进行编译,该选项会禁用sqrt
的旧System V错误代码(IEEE-754异常仍然有效)。
答案 1 :(得分:1)
经过各种额外的实验,我确信自己一个简单的参数变换不会使用比参数和结果更高的精度,它不能达到比第一个变量更高的误差范围。我发布的代码。
由于我的问题是尽量减少因log1pf()
本身的错误而引起的参数转换的错误,因此用于实验的最直接的方法是利用正确舍入该对数函数的实现。请注意,在高性能环境的上下文中,不太可能存在正确舍入的实现。根据J.-M.的作品穆勒等。另外,为了产生准确的单精度结果,x86扩展精度计算应该足够了,例如
float accurate_log1pf (float a)
{
float res;
__asm fldln2;
__asm fld dword ptr [a];
__asm fyl2xp1;
__asm fst dword ptr [res];
__asm fcompp;
return res;
}
使用我的问题中的第一个变体asinhf()
的实现,如下所示:
float my_asinhf (float a)
{
float fa, s, t;
fa = fabsf (a);
if (fa >= 0x1.0p64f) { // prevent overflow in intermediate computation
t = log1pf (fa) + 0x1.62e430p-1f; // log(2)
} else {
t = fmaf (fa / (1.0f + sqrtf (fmaf (fa, fa, 1.0f))), fa, fa);
t = accurate_log1pf (t);
}
return copysignf (t, a); // restore sign
}
使用所有2个 32 IEEE-754单精度操作数进行测试表明,最大误差为1.49486070 ulp发生在±0x1.ff5022p-9
处,并且有353,521,140个错误的舍入结果。如果整个参数变换使用双精度算术会发生什么?代码更改为
float my_asinhf (float a)
{
float fa, s, t;
fa = fabsf (a);
if (fa >= 0x1.0p64f) { // prevent overflow in intermediate computation
t = log1pf (fa) + 0x1.62e430p-1f; // log(2)
} else {
double tt = fa;
tt = fma (tt / (1.0 + sqrt (fma (tt, tt, 1.0))), tt, tt);
t = (float)tt;
t = accurate_log1pf (t);
}
return copysignf (t, a); // restore sign
}
但是,此更改不会改善错误限制!最大误差1.49486070 ulp仍然出现在±0x1.ff5022p-9
,现在有350,971,046个错误的舍入结果,比之前略少。问题似乎是float
操作数无法向log1pf()
传达足够的信息以产生更准确的结果。计算sinf()
和cosf()
时会出现类似的问题。如果将表示为正确舍入的float
操作数的简化参数传递给核心多项式,则sinf()
和cosf()
中的结果错误仅略低于1.5 ulp,就像我们一样正在观察my_asinhf()
。
一种解决方案是将变换后的参数计算为高于单精度,例如作为双浮点操作数对(可以在this paper by Andrew Thall中找到有关双浮点技术的简要概述)。在这种情况下,我们可以使用附加信息对结果进行线性插值,这是基于对数的导数是倒数的知识。这给了我们:
float my_asinhf (float a)
{
float fa, s, t;
fa = fabsf (a);
if (fa >= 0x1.0p64f) { // prevent overflow in intermediate computation
t = log1pf (fa) + 0x1.62e430p-1f; // log(2)
} else {
double tt = fa;
tt = fma (tt / (1.0 + sqrt (fma (tt, tt, 1.0))), tt, tt);
t = (float)tt; // "head" of double-float
s = (float)(tt - (double)t); // "tail" of double-float
t = fmaf (s, 1.0f / (1.0f + t), accurate_log1pf (t)); // interpolate
}
return copysignf (t, a); // restore sign
}
此版本的详尽测试表明最大误差已减少到0.99999948 ulp,它出现在±0x1.deeea0p-22
。有349,653,534个错误的舍入结果。已经实现了asinhf()
的忠实执行。
不幸的是,这个结果的实际效用是有限的。根据硬件平台,double
上算术运算的吞吐量可能只是float
运算吞吐量的1/2到1/32。双精度计算可以用双浮点计算代替,但这也会产生非常大的成本。最后,我的方法是使用单精度实现作为后续双精度工作的试验场,许多硬件平台(当然是我感兴趣的所有硬件平台)都没有为更高精度的数字格式提供硬件支持。比IEEE-754 binary64
(双精度)。因此,任何解决方案都不应该在中间计算中需要更高精度的算法。
由于在asinhf()
的情况下所有麻烦的论点都很小,人们可以[部分地]通过对原点周围区域使用多项式极小极大近似来解决精度问题。由于这将创建另一个代码分支,它可能会使矢量化更加困难。