与Matlab比较,获得三角函数的正确值

时间:2018-09-07 20:43:20

标签: c++ matlab floating-point ieee-754 mpfr

我正在尝试使用C ++代码测试simulink块,该simulink块包含一些代数,三角函数和积分器。在我的测试过程中,从simulink块输入中使用了一个随机数生成器,并将输入和输出都记录到Mat文件中(使用MatIO),该文件将由C ++代码读取,并将输出与C ++计算值进行比较。对于仅包含代数函数的信号,结果是精确的,且差为零;对于包含三角函数的路径,其差约为10e-16。 Matlab社区声称它们是正确的,而glibc不是。

最近,根据老问题1 2 3,我发现在glibc中实现的三角函数的输出值不等于在MATLAB中产生的值,而我的实验表明1ulp> glibc的准确性。对于大多数块而言,这个10e-16误差并没有多大意义,但是在积分器的输出中,10e-16越来越多地累积,积分器的最终误差将约为1e-3,这有点高,并且这种类型的屏蔽是不可接受的。

经过大量研究,我决定使用glibc中提供的其他方法来计算sin / cos函数。

我实现了这些方法,

具有长双变量和-O2的1-taylor系列(强制使用x87 FPU及其80位浮点运算法则)

2- taylor系列,带有GNU Quadmath库(128位精度)

3- MPFR库(128位)

4- CRLibm(正确舍入的libm)

5- Sun的LibMCR(就像CRLibm一样)

6-具有不同舍入模式的X86 FSIN / FCOS

7-通过JNI的Java.lang.math(我认为Matlab使用)

8- fdlibm(根据我所见的博客文章之一)

9- openlibm

10-通过mex / matlab引擎调用matlab函数

除了最后一个实验无法产生等于matlab的值外,其他所有实验均没有。我测试了所有这些库和方法的输入范围,其中某些库(例如libmcr和fdlibm)将为某些输入生成NAN值(看起来它们没有良好的范围检查),其余的则生成带有错误为10e-16及更高。 与上次提到的matlab相比,只有最后一个产生正确的值,但是调用matlab函数效率不高,并且比本地实现慢得多。

我也惊讶为什么长双倍和四边形的MPFR和taylor系列出现错误。

这是泰勒级数,具有长双变量(80位精度) 并且应使用-O2进行编译,以防止将FPU堆栈中的值存储到寄存器中(80位至64位=精度损失),并且在进行任何计算之前,x87的舍入模式将设置为最接近

typedef long double dt_double;

inline void setFPUModes(){
    unsigned int mode = 0b0000111111111111;
    asm(

    "fldcw %0;"
    :  : "m"(mode));
}
inline dt_double factorial(int x)  //calculates the factorial
{
    dt_double fact = 1;   
    for (; x >= 1 ; x--)
        fact = x * fact;
    return fact;
}

inline dt_double power(dt_double x, dt_double n) //calculates the power of x
{
    dt_double output = 1;
    while (n > 0)
    {
        output = (x * output);
        n--;
    }
    return output;
}

inline double sin(double x) noexcept  //value of sine by Taylors series
{
    setFPUModes();

    dt_double result = x;

    for (int y = 1 ; y != 44; y++)
    {
        int k = (2 * y) + 1;
        dt_double a = (y%2) ? -1.0 : 1.0;
        dt_double c = factorial(k);
        dt_double b = power(x, k);

        result = result + (a * b) / c;
    }
    return result;
}

使用x87的所有四种舍入模式对taylor级数方法进行了测试,最好的一种具有10e-16的误差

这是X87 fpu之一

double sin(double x) noexcept
{
    double d;
    unsigned int mode = 0b0000111111111111;
    asm(
    "finit;"
    "fldcw %2;"
    "fldl %1;"
    "fsin;"
    "fstpl %0" :
    "+m"(d) : "m"(x), "m"(mode)
      );

    return d;
}

x87 fpu代码也不比上一个更准确

这是MPFR的代码

 double sin(double x) noexcept{
    mpfr_set_default_prec(128);
    mpfr_set_default_rounding_mode(MPFR_RNDN);
    mpfr_t t;
    mpfr_init2(t, 128);
    mpfr_set_d(t, x, MPFR_RNDN);

    mpfr_t y;
    mpfr_init2(y, 128);
    mpfr_sin(y, t, MPFR_RNDN);

    double d = mpfr_get_d(y, MPFR_RNDN);

    mpfr_clear(t);
    mpfr_clear(y);

    return d;
}

我不明白为什么MPFR版本无法按预期运行

我测试的所有其他方法的代码也相同,并且与matlab相比,所有方法都有错误。

所有代码都经过了广泛的数字测试,我发现它们失败的简单情况。例如:

在matlab中,以下代码生成0x3fe1b071cef86fbe,但在这些方法中,我得到了0x3fe1b071cef86fbf(最后一位的差异)

format hex;
sin(0.5857069572718263)
ans = 0x3fe1b071cef86fbe

要清楚这个问题, 如上所述,这一点的不准确性在将其馈入积分器时很重要,我正在寻找一种解决方案以获取与Matlab完全相同的值。有什么想法吗?

更新1:

1 Ulp错误根本不影响算法的输出,但是特别是在积分器的输出中,它防止了用matlab结果进行验证。

如@John Bollinger所说,错误不会在具有多个算术块的直接路径中累积,但不会在馈入离散积分器的情况下累积

Update2: 我计算了上述所有方法的不相等结果的数量,显然,与matlab相比,openlibm产生的不相等值更少,但是它不是零。

1 个答案:

答案 0 :(得分:5)

我的猜测是Matlab使用的是最初基于FDLIBM的代码。我能够使用Julia(使用openlibm)获得相同的结果:您可以尝试使用它,也可以尝试使用musl,我相信它也使用相同的代码。

与0.5857069572718263最接近的double / IEEE binary64是

0.5857069572718263117394599248655140399932861328125

(具有位模式0x3fe2be1c8450b590

sin

0.55278864311139114312806521962078480744570117018100444956428008387067038680572587 ...

与此最接近的两个double / IEEE binary64是

a)0.5527886431113910870038807843229733407497406005859375(0x3fe1b071cef86fbe),其错误为0.5055 ulps

b)0.55278864311139119802618324683862738311290740966796875(0x3fe1b071cef86fbf),其误差为0.4945 ulps

FDLIBM仅保证正确至<1 ulp,因此任一个都是可以接受的,并且碰巧返回(a)。 crlibm正确取整,glibc provides a tighter guarantee为0.55 ulps,因此两者都将返回(b)。