从log1pf()计算asinhf()的最准确方法是什么?

时间:2015-12-30 18:42:11

标签: c math floating-point floating-accuracy

反双曲函数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%,正确圆润。定向舍入的基本算术可以,无需额外的性能成本。

2 个答案:

答案 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()的情况下所有麻烦的论点都很小,人们可以[部分地]通过对原点周围区域使用多项式极小极大近似来解决精度问题。由于这将创建另一个代码分支,它可能会使矢量化更加困难。