1. 概述
计算机中的小数分为定点数
和浮点数
,定点数比较简单,浮点数比较复杂,也是面试中常常被问到的问题,很多资料都讲得很简单,一笔带过,没有深入讲解,在本文中将对浮点数的误差问题做一个比较详细的深入分析。
2. IEEE754
标准
计算机中任何事都需要一个标准,处理浮点数存储的标准最通用的标准就是IEEE754
标准,该标准规定了4种浮点数类型:单精度
、双精度
、延伸单精度
、延伸双精度
,其中最常用的是单精度
和双精度
,如下表所示:
精度 | 字节数 | 正数取值范围 | 负数取值范围 |
---|---|---|---|
单精度类型 | 4 | 1.4e-45 至 3.4e+38 | -3.4e+38 至 -1.4e-45 |
双精度类型 | 8 | 4.9e-324 至 1.798e+308 | -1.798e+308 至 -4.9e-324 |
我们以单精度浮点数
(4字节,32位)为例来说明其原理,双精度浮点数
只是位数更多,原理类似。
浮点数都是以科学计数法
的方式存储数字的,它由符号
、有效数字
和指数
三部分组成,在IEEE中换了个名字,分别为:符号
、阶码
、尾数
,存储其值得对应就是符号位
、阶码位
和尾数位
.
单精度浮点数
占 32
位,最高位为符号位,占 1
位;接着为占 8
位的指数位,最后为占 23
位的尾数位,如下图所示:
3. 浮点数转化为二进制形式
计算机只能存储二进制数据,因此浮点数在存储时先将其转化为二进制的形式。
1)整数部分
整数部分转化比较好转化,除2取余数即可。
2)小数部分
小数部分转化为二进制跟整数部分不一样,小数部分每次乘2:如果大于1,则添加一个1
作为二进制位,并将相乘得到的结果中整数部分的1去掉,继续;如果小于0,则添加一个0
作为二进制位,继续。
例如,16.35
转化为二进制的过程如下:
- 整数部分为16,其对应二进制位
10000
. - 小数部分为0.25,转化过程如下图所示:
因此,0.35
所得的二进制为0.010110011001100110011001100...(1100的无限循环)
所以,16.35
所得的二进制为 10000.0101100110011001100110011001100...
4. 规格化表示
十进制的科学技术法一般将整数位规格化为 |a|, 1 <= |a| < 10
,在浮点数的存储时,也进行规格化,使得整数部分为1
,如上面的16.35
的规格化表示为:
2^4 * 1.0000010110011001100110011001100...
5. 存储
符号
根据前面的IEEE754标准存储,最高位为符号位,0
表示正数,1
表示负数
阶码
阶码采用 移码
的形式存储,如下公式可得到移码
:E = e + (2^(n - 1) - 1)
,e为指数的真实值,阶码为E,n为位数。
移码
即是将整体全部向正方向移动使得所有值都映射到正数上。
移码
可以表示的指数范围为[-126, 127]
,(因为全0被认为是机器零,全1被认为是无穷大,也正是此原因,在计算移码时e+(2^(n-1)-1)
而不是e+(2^(n-1))
)。
例如,上面16.35
的指数为4,其对应的移码为127 + 4 = 131 (10000011
)
尾数
尾数,即为有效数字部分,因为在规格化表示的时候已经使得整数部分为 1
,因此,在存储时可以略去这个1
以节省存储空间。
IEEE754规定,尾数以原码
的形式存储,例如上面的16.35
的尾数部分为:
00000101100110011001101
(注意最后那里,这里涉及到舍入的问题,将在后面详细讲解,其他很多书都直接略去了这里)
因此,可以得到16.35
在IEEE754标准下存储的完整形式的二进制码为
6. 舍入方式
在二进制形式表示小数的时候,有时是尾数无限循环或则23位无法将其存储,这就会导致浮点数的误差
问题。
如果直接将其舍去,那么二进制存储的数值只会比真实的浮点数的值小
但是,在平时的实践中,我们知道,浮点数有时候比其真实值小,有时又比其真实值大,这是什么原因导致的呢?
这是因为在存储时采用了舍入
的方式来得到其近似值。
IEEE754定义了4种舍入方式:向偶数舍入、向零舍入、向下舍入、向上舍入。
方式 | 1.40 | 1.60 | 1.50 | 2.50 | -1.50 |
---|---|---|---|---|---|
向偶 | 1 | 2 | 2 | 2 | -2 |
向零 | 1 | 1 | 1 | 2 | -1 |
向下 | 1 | 1 | 1 | 2 | -2 |
向上 | 2 | 2 | 2 | 3 | -1 |
很小的误差在四舍五入时,如果在进行大数据量的统计时产生的误差累积将会非常大。
向偶数舍入的方式使得在大多数情况下,5舍去还是进位的概率是差不多的,在进行一些大量数据的统计时产生的偏差相对其他方式小一些。因此,向偶舍入最常采用。
例如:
保留三位小数
(1). 1.001011
舍入后为: 1.001
.原因:1.001011
舍入后有两个选择: 1.001
和1.010
.
|1.001011 - 1.001
| = 0.000011
,|1.001011 - 1.010
| = 0.000101
,显然,0.000011
< 0.000101
,所以:1.001
比1.0101
更接近原值1.001011
,所以舍入得到了1.001
.
(2). 1.001101
舍入后为: 1.010
.原因:1.001101
舍入后有两个选择: 1.001
和1.010
.
|1.001101 - 1.001
| = 0.000101
,|1.001101 - 1.010
| = 0.000011
,显然后者更小,因此舍入得到的是0.010
.
(3). 1.001100
舍入后为: 1.010
.原因:1.001100
舍入后有两个选择: 1.001
和1.010
.
|1.001100 - 1.001
| = 0.000100
,|1.001100 - 1.010
| = 0.000100
,两种选择的差值是相同的,这时使用向偶舍入
的方式,1.010
是偶数(0偶1奇),所以舍入得到了0.010
.
7. 例子
在前面的16.35
的尾数中,后面是1100
无限循环,舍入后为
而不是
如下的代码
public class Test {
public static void main(String[] args) {
float a = 16.35f;
float b = a - 16;
System.out.println(b);
}
}
运行结果为:
16.35 - 16
的结果应该为0.35
,结果却为0.35000038
,正是因为上面讲到的舍入产生的。
16.35
与16
的IEEE754标准存储的二进制相减并结果规格化后得到的二进制码为:
阶码为 01111101
,即为-2
尾数为 1.01100110011001100000000
,计算其十进制值为:
1 + 2^(-2) + 2^(-3) + 2^(-6) + 2^(-7) + 2^(-10) + 2^(-11) + 2^(-14) + 2^(-15) + 2^(-17)
= 1.4000015258789062
1.4000015258789062 * (2^(-2))
= 0.35000038146972656
保留默认的8位小数得到0.35000038
,跟以上程序得到的结果一致。
8. 小结
计算机中浮点数的误差是由于在存储的时候存在舍入
导致的,舍入
可以导致二进制保存值大于真实值,也有可能使得保存的值小于真实值,一般采用的是向偶舍入的方法。
参考文献
[1] https://blog.leodots.me/post/45-ieee754-rounding-rules.html