与matlab相比,获得三角函数的正确值

问题描述 投票:1回答:1

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

最近我发现在glibc中实现的三角函数的输出值不等于matlabs中产生的值,根据旧问题1 2 3和我的实验,差异相关1ulp> glibc的准确性。对于大多数块而言,这个10e-16误差感觉不大,但是在积分器的输出中,10e-16积累的越来越多,积分器的最终误差将是大约1e-3,这有点高,这种阻止是不可接受的。

经过对该问题的大量研究后,我决定使用其他方法来计算sin / cos函数,而不是glibc中提供的函数。

我实施了这些方法,

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

2-taylor系列与GNU quadmath库(128位精度)

3个MPFR库(128位)

4- CRLibm(正确舍入的libm)

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

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

7- Java.lang.math到JNI(我认为matlab使用)

8-fdlibm(根据我见过的一篇博文)

9开放的libm

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

除了最后一个以外的实验不能生成等于matlab的值。我测试了所有这些库和方法的广泛输入,其中一些像libmcr和fdlibm将为一些输入产生NAN值(看起来他们没有良好的范围检查),其余的产生值错误10e-16及更高。与matlab相比,只有最后一个产生正确的值,但是调用matlab函数并不比本机实现快得多且慢得多。

另外我还说为什么MPFR和泰勒系列长双重和四重奏都出错了。

这是具有长双变量(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的所有四种舍入模式测试的泰勒系列方法,最好的一种方法具有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,但在这些apporoches中我得到了0x3fe1b071cef86fbf(最后一位的差异)

format hex;
sin(0.5857069572718263)
ans = 0x3fe1b071cef86fbe

要明确这个问题,正如我上面所描述的那样,当它输入积分器并且我正在寻找一种解决方案来获得与matlab完全相同的值时,这一点不准确是很重要的。有任何想法吗?

UPDATE1:

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

正如@John Bollinger所说,误差不会累积在具有多个算术块的直接路径中,而是在馈入离散积分器时不会累积

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

c++ matlab floating-point ieee-754 mpfr
1个回答
5
投票

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

最接近的double / IEEE二进制64到0.5857069572718263是

0.5857069572718263117394599248655140399932861328125

(有位模式0x3fe2be1c8450b590

这是sin

0.55278864311139114312806521962078480744570117018100444956428008387067038680572587...

这两个最接近的double / IEEE二进制64是

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)。

© www.soinside.com 2019 - 2024. All rights reserved.