【发布时间】:2016-04-04 18:25:26
【问题描述】:
反双曲函数asinh()与自然对数密切相关。我试图确定从 C99 标准数学函数 log1p() 计算 asinh() 的最准确方法。为了便于实验,我现在将自己限制为 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() 实现,我在对所有 232 个可能的 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%,都是正确四舍五入的。具有定向舍入的基本算术可能可以在不增加性能成本的情况下使用。
【问题讨论】:
-
如果你想要准确,放弃
float并尽快转移到double,如果你的编译器支持80位实数表示,甚至是long double。 -
出于性能原因,这不是一个现实的选择,所有中间计算都必须以输入和结果的“本机精度”进行。我会将该要求编辑到问题中。
-
“本地”是指“与函数原型中使用的类型相同”。在常用的平台 (GPU) 中,
float操作的吞吐量可以显着高于double操作,并且在包括带有 AVX 的 x86 在内的许多平台上,没有方便的方法来使用比double. -
是的,我正在考虑的所有平台都支持
double,但它可能比float慢32 倍,因此考虑到高性能的需要,这并不实用。另外我使用float版本作为double版本的实验平台,除了double之外没有任何更高精度的硬件支持 -
我希望在规定的约束条件下获得最佳精度,也就是说,不要完全搞砸性能。所写的问题是关于
float案例,但我打算稍后为double重复使用相同的算法,正如我在问题开头提到的那样。
标签: c math floating-point floating-accuracy