cosf(M_PI_2)不返回零
今天早上突然开始。
这是原创的
float angle = (x+90)*(M_PI/180.0); float xx = cosf(angle); float yy = sinf(angle);
放入断点并hover光标后..我得到yy的正确答案为1.但xx不为零。
我尝试过cosf(M_PI_2);
仍然没有运气..它工作正常,直到昨天..我没有改变任何编译器设置等..
我正在使用截至今天的Xcode最新版本
与其他人所说的相反,这不是 x87协处理器问题。 默认情况下,XCode在Intel上使用SSE进行浮点计算( long double
精度算法除外)。
“问题”是:当您编写cosf(M_PI_2)
,您实际上是在告诉XCode编译器(gcc或llvm-gcc或clang)执行以下操作:
- 在
查找M_PI_2
的扩展。 根据POSIX标准,它是一个双精度字面值,可以转换为正确舍入的π/ 2值。 - 将转换后的双精度值舍入为单精度。
- 在单精度值上调用数学库函数
cosf
。
请注意,在整个过程中,您不会对π/ 2的实际值进行操作。 相反,您将对该值进行操作,舍入为可表示的浮点数。 虽然cos(π/ 2) 恰好为零,但您并没有告诉编译器进行该计算。 你反而告诉编译器做cos(π/ 2 + tiny),其中tiny是舍入值(float)M_PI_2
和π/ 2的( (float)M_PI_2
的)精确值之间的差值。 如果计算cos
而没有任何误差,则cos(π/ 2 +微小)的结果约为-tiny。 如果它返回零, 那将是一个错误。
编辑:使用当前的XCode编译器逐步扩展Intel Mac上的计算:
M_PI_2
被定义为
1.57079632679489661923132169163975144
但这实际上并不是一个可表示的双精度数。 当编译器将其转换为双精度值时,它就会完全变为
1.5707963267948965579989817342720925807952880859375
这是最接近π/ 2的双精度数,但它与π/ 2的实际数学值相差约6.12 * 10 ^( – 17)。
步骤(2)将此数字舍入为单精度,这会将值精确地更改为
1.57079637050628662109375
大约是π/ 2 + 4.37 * 10 ^( – 8)。 当我们计算这个数字的cosf
时,我们得到:
-0.00000004371138828673792886547744274139404296875
这几乎是在此时评估的余弦的精确值:
-0.00000004371139000186241438857289400265215231661...
事实上,这是正确的圆润结果 ; 计算可能没有返回更准确的值。 这里唯一的错误是您要求编译器执行的计算与您认为要求它执行的计算不同。
首先要注意的是你使用的是float
。 这些本质上是不准确的,并且对于大多数计算,只给出了数学上正确答案的近似值。 假设代码中的x
值为0
,则angle
将近似于π/ 2。 因此, xx
将近似为cos(π/ 2)。 然而,由于近似和舍入问题,这不可能完全为零。
如果你能够将你的代码更改为我们的代码而不是float
代码,那么你可能会获得更高的准确性,并且答案会接近零。 但是,如果此时代码产生的值非常重要,那么您将不得不重新考虑如何进行计算。
如果这不能解决您的特定问题,请向我们提供更多详细信息,我们会另外考虑一下。
我怀疑答案尽可能接近0而不值得担心。
如果我运行相同的东西我得到答案“-4.3711388e-008”,也可以写成“-0.000000043711388”。 这是非常接近0.非常接近,不用担心它在小数点后8位。
编辑:继续LiraLuna的说法,我在visual studio下编写了下面的x87汇编程序
float fRes; _asm { fld1 fld1 fadd st, st(1) fldpi fdiv st, st(1) fcos fstp [fRes] } char str[16]; sprintf( str, "%f", fRes );
基本上,这使用x87的fcos指令来执行pi / 2的余弦。 str中保存的值为“0.000000”
然而,这实际上并不是fcos返回的内容。 它实际返回6.1230318e-017。 这意味着错误发生在第17个小数位,而且说实话,这远比上面的标准调试cosf重要。
由于SSE3没有特定的余弦指令,我怀疑(虽然我不能确定没有看到汇编器生成)它是使用自己的taylor系列扩展还是使用fcos指令。 在我看来,无论哪种方式,你仍然不可能获得比在小数点后17位发生的错误更好的精度。
我唯一能想到的是恶意宏观替换,即M_PI_2
不再是1.57079632679489661923。
尝试调用cosf( 1.57079632679489661923 )
来测试它。
你应该注意的真实是余弦的标志 。 确保它与您的预期相同。 例如,如果您使用0到pi / 2之间的角度操作。 确保 您用作PI_2的内容小于pi / 2的实际值 !
并且0.000001
和0.0
之间的差异小于您的想法。
原因
你正在经历的是臭名昭着的x87数学协处理器浮动截断’bug’ – 或者更确切地说 – 一个function。 IEEE float
具有惊人的数字范围,但需要付出代价。 他们牺牲了高射程的进动。
然而,它们并不像你想象的那样不准确 – 这是英特尔x87芯片设计产生的半神话,它内部使用80位内部表示浮动 – 它们虽然有点慢,却具有更优越的进动性。
当您执行浮点比较时,x87将浮点缓存为80位浮点数,然后当它的堆栈已满时,它会将32位表示保存在RAM中,从而在很大程度上降低精度。
解决方案
x87很旧,很老。 它的替代品是SSE 。 SSE原生计算32位浮点数和64位浮点数,导致数学上的最小进动损失。 请注意浮点数的进动问题仍然存在,但printf("%f\n", cosf(M_PI_2));
应该是零。 哎呀 – 甚至与SSE的浮动比较再次准确! (与x87不同)。
由于最新的Xcode实际上是GCC 4.2.1,使用编译器开关-msse3 -mfpmath=sse
,看看你如何得到一个完美的圆形0.00000
(注意:如果你得到-0.00000
,不用担心,它完全没问题,仍然等于0.00000
以下IEEE规范(在此维基百科文章中阅读更多内容))。
所有英特尔mac都保证具有SSE3支持(OSx86 Mac排除,如果你想支持它们,请使用-msse2
)。