“要想为某些正确选择数的精度具有重要意义的问题选择一种正确的精度,就需要有对浮点计算的深入理解。如果你并没有这种理解,那么给你一个忠告是花时间去学习,或者就选择double并期待着能得到最好的结果”
--------------------The C++ Programming Language
因为这句话,我今天好好的了解了一把浮点数的相关问题。
首先我们得了解浮点数在计算机内的表示方法。据说有这么一个感人的故事:
1985年Intel打算为其8086微处理器引进一种浮点数协处理器的时候,请加州大学伯克利分校的 William Kahan教授──最优秀的数值分析家之一来为8087 FPU设计浮点数格式; 而这个家伙又找来两个专家来协助他,于是就有了KCS组合(Kahn, Coonan, and Stone)。 他们共同完成了Intel的浮点数格式设计,而且完成地如此出色,以致于IEEE组织决定采用一个非常接近KCS的方案作为IEEE的标准浮点格式。这也就是传说中的IEEE754标准格式。目前,几乎所有计算机都支持该标准,大大改善了科学应用程序的可移植性。
按照IEEE754标准,32位和64位浮点数的标准格式为:
S 为符号位,0为正,1为负。E(exponent)和M(mantissa)分别表示阶码和尾数。怎么来的?我们知道10进制
有科学计数法,二进制数也类似。一个规格化的32位浮点数x真值表示为
x = (-1)S * (1.M) * 2 (E - 127)
这方面的详细请参照计组原理。这里我们可以看出E的位数决定了浮点数范围,M的位数决定其精度。
(一)精度来源
为什么会有精度问题,这是我们第一个要讨论的。
首先我们得知道实数是一个连续的无穷集合,即使是范围在-1 - 1之间的实数组成的集合也是无穷的。而由浮点数表示方法来看,对于确定阶码和尾数位数的浮点数,其个数是有限的。同时随着阶码的增大,相邻俩浮点数间的 距离会越来越大。单精度浮点数最大值为m1=340282346638528870000000000000000000000.0,次大值为 m2=340282326356119250000000000000000000000.0.这两数之差就是单精度浮点数之间的最大间隔. m1-m2=20282409603651670000000000000000.0
在m1和m2之间的实数都不能精确存储,一般采用靠的最近的那个数表示。由此可见可精确存储的数反而是很少的,绝大多数的数的存储都会存在精度损失。
这里举一个实例,代码如下:
cout << 0.2 * 3 << " " << 0.6 << endl;
cout << (0.2 * 3 == 0.6) << endl;
输出结果为:
0.6 0.6
0
这就是因为精度损失导致的,如果我们忽略的话,可能造成一些很难发现的错误。
(二)有效位数
在之前说基本数据类型时我们发现,C++11在给出数据尺寸最小值时一般规定的是计算机存储该类型时使用的最小空间,只有在规定浮点数时说的是最小有效位数。这是因为在实际使用时,我们最关注的是浮点数的有效位数。根据
IEEE754标准,单精度浮点数尾数位数是23,加上一位隐藏的1,总共24位。2的24次方位16777216,介于10的7次方和8次方之间,故可表示的10进制有效位数位7位。同理可得出双精度浮点数的有效位数为15。
注:这里说一个小技巧,可被精确存储的十进制小数具备的必要不充分条件为:最后一位必须是5。因为只有可被表示成2的-t次方和(t在1到23或52之间)的数才能被精确存储。
(三)使用时应注意的地方
1. 我认为最重要的一点:根据运算结果的估算有效位数选择单精度或双精度。
例如:53.9 * 78.5,可估计整数部分有4位,小数部分有2位,故要求有效位数至少6位,可用float型表示。
2. 我认为灰常重要的一点:俩个浮点数进行等值比较时不能直接用 “==”。如之前说的例子,由于0.2和0.6都不能精确存储(这里你应该明白为什么吧),导致结果输出为 0。那怎么办呢?
abs(0.2 * 3 - 0.6) < 1E-6。 没错,就是俩数之差的绝对值和一个极小的数进行比较,结果成立,则可认为俩数相等。由于单双精度不同,一般单精度和1E-6比,双精度和1E-14比。
3.还有一些例如浮点数累加是误差会累积,较大的浮点数和较小的浮点数进行加减运算时可能结果和叫大数一样
之类的问题,我想你了解原理之后应该会知道原因。平时多留意一下,多积累一点经验。欢迎来和我分享!
4.一般说法是double运算不比float慢,且某些机器上double更快,故通常多用double,既保证精度,又保证速度。
(至于原理,再聊哈)