详解计算机中浮点数存储、运算及其误差问题

1. 概述

计算机中的小数分为定点数浮点数,定点数比较简单,浮点数比较复杂,也是面试中常常被问到的问题,很多资料都讲得很简单,一笔带过,没有深入讲解,在本文中将对浮点数的误差问题做一个比较详细的深入分析。


2. IEEE754标准

计算机中任何事都需要一个标准,处理浮点数存储的标准最通用的标准就是IEEE754标准,该标准规定了4种浮点数类型:单精度双精度延伸单精度延伸双精度,其中最常用的是单精度双精度,如下表所示:

精度字节数正数取值范围负数取值范围
单精度类型41.4e-45 至 3.4e+38-3.4e+38 至 -1.4e-45
双精度类型84.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转化为二进制的过程如下:

  1. 整数部分为16,其对应二进制位10000.
  2. 小数部分为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.401.601.502.50-1.50
向偶1222-2
向零1112-1
向下1112-2
向上2223-1

很小的误差在四舍五入时,如果在进行大数据量的统计时产生的误差累积将会非常大。

向偶数舍入的方式使得在大多数情况下,5舍去还是进位的概率是差不多的,在进行一些大量数据的统计时产生的偏差相对其他方式小一些。因此,向偶舍入最常采用。

例如:
保留三位小数
(1). 1.001011舍入后为: 1.001.原因:1.001011舍入后有两个选择: 1.0011.010.

|1.001011 - 1.001| = 0.000011,|1.001011 - 1.010| = 0.000101,显然,0.000011 < 0.000101,所以:1.0011.0101更接近原值1.001011,所以舍入得到了1.001.

(2). 1.001101舍入后为: 1.010.原因:1.001101舍入后有两个选择: 1.0011.010.

|1.001101 - 1.001 | = 0.000101,|1.001101 - 1.010| = 0.000011,显然后者更小,因此舍入得到的是0.010.

(3). 1.001100舍入后为: 1.010.原因:1.001100舍入后有两个选择: 1.0011.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.3516的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

  • 7
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值