我正在尝试使用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产生的不相等值更少,但是它不是零。
答案 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)。