浮点数为何做不到精确计算?

1 浮点数为何做不到精确计算?

众所周知,Java中的浮点数在进行四则运算时,大概率是得不到一个精确值的,只能得到一个十分接近精确值的近似值,为什么会出现这种情况呢?接下来就让我们来探讨一下这个问题吧。

2 浮点数的存储规则

我们都知道,计算机中存储的都是二进制数,浮点数也不例外。那么浮点数是如何存储的呢?根据IEEE(电子和电气工程师协会)规定,任意一个浮点数N都可以用如下科学计数法表示: N = ( − 1 ) S ∗ M ∗ 2 E N=(-1)^S*M*2^E N=(1)SM2E
其中,

  • 符号(sign):S决定正数(S=0)还是负数(S=1);
  • 有效数字(significant):M是一个二进制小数,它的范围是1≤M<2;
  • 阶码(exponent):E表示指数(可能是负数)。

举例来说,十进制的3.0,写成二进制是11.0,也就是 1.1 ∗ 2 1 1.1*2^1 1.121,按照上面的格式,可以得到S=0,M=1.1,E=1。
IEEE754规定,对于32位的浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M,如下图所示:
bit32
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位位有效数字M,如下图所示:
bit64
除此之外,IEEE754对于有效数字M和指数E还有一些特别规定:

  1. 前面说过,1≤M<2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。IEEE754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
  2. 至于指数E,情况就比较复杂。首先,E为一个无符号整数(unsigned int)。这意味着,如果E为8位,它的取值范围为0255;如果E为11位,它的取值范围为02047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE754规定,E的真实值必须再减去一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如, 2 10 2^{10} 210的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
  3. 然后,指数E还可以再分成三种情况:
    1. E不全为0或不全为1。这时,浮点数就采用上面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
    2. E全为0。这时,浮点数的指数E等于1-127(或者1-1023),有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
    3. E全为1。这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);如果有效数字M不全为0,表示这个数不是一个数(NaN)。

而Java是完全按照IEEE754存储浮点数的,我们可以通过如下代码查看浮点数对应的二进制数

System.out.println(Integer.toBinaryString(Float.floatToIntBits(0.05f))); 

可以看到控制台出现111101010011001100110011001101这串二进制数,将它补齐到32位再按照IEEE754规定进行拆分,得到S=0,E=01111010,对应10进制的122,122-127得到E的真实值为-5,M=10011001100110011001101,给它补上第一位的1得到M=1.10011001100110011001101,然后再右移5位得到0000110011001100110011001101。接下来我们手动将十进制的0.05转成二进制,如下图所示:
小数转二进制
可以看到十进制的0.05转成二进制后是一串无限循环数0000110011…,对比之前得到的那串可以看出Java是简单的将后续部分的二进制数舍去了。

3 浮点数的加减运算

Java中的浮点计算底层是如何进行的呢?我们来看一个例子:

System.out.println(0.05f + 0.01f);

可以看到控制台打印出了0.060000002,为什么会得到这个数字呢?我们需要知道,浮点数的加减运算一般由以下五个步骤完成:对阶尾数运算规格化舍入处理溢出判断

3.1 对阶

所谓对阶是指将两个进行运算的浮点数的阶码对齐的操作。对阶的目的是为使两个浮点数的尾数能够进行加减运算。因为,当进行 M x ∗ 2 E x M_x*2^{E_x} Mx2Ex M y ∗ 2 E y M _y*2^{E_y} My2Ey加减运算时,只有使两浮点数的指数值部分相同,才能将相同的指数值作为公因数提出来,然后进行尾数的加减运算。对阶的具体方法是:首先求出两浮点数阶码的差,即 ⊿ E = E x − E y ⊿E=E_x-E_y EExEy,将小阶码加上⊿E,使之与大阶码相等,同时将小阶码对应的浮点数的尾数右移相应位数,以保证该浮点数的值不变。几点注意:
(1)对阶的原则是小阶对大阶,之所以这样做是因为若大阶对小阶,则尾数的数值部分的高位需移出,而小阶对大阶移出的是尾数的数值部分的低位,这样损失的精度更小。
(2)若⊿E=0,说明两浮点数的阶码已经相同,无需再做对阶操作了。
(3)采用补码表示的尾数右移时,符号位保持不变。
(4)由于尾数右移时是将最低位移出,会损失一定的精度,为减少误差,可先保留若干移出的位,供以后舍入处理用。

3.2 尾数运算

尾数运算就是进行完成对阶后的尾数相加减。这里采用的就是我们前面讲过的纯小数的定点数加减运算。

3.3 结果规格化

在机器中,为保证浮点数表示的唯一性,浮点数在机器中都是以规格化形式存储的。对于IEEE754标准的浮点数来说,就是尾数必须是1.M的形式。由于在进行上述两个定点小数的尾数相加减运算后,尾数有可能是非规格化形式,为此必须进行规格化操作。
规格化操作包括左规和右规两种情况。
左规操作:将尾数左移,同时阶码减值,直至尾数成为1.M的形式。例如,浮点数 0.0011 ∗ 2 5 0.0011*2^5 0.001125是非规格化的形式,需进行左规操作,将其尾数左移3位,同时阶码减3,就变成 1.1100 ∗ 2 2 1.1100*2^2 1.110022规格化形式了。
右规操作:将尾数右移1位,同时阶码增1,便成为规格化的形式了。要注意的是,右规操作只需将尾数右移一位即可,这种情况出现在尾数的最高位(小数点前一位)运算时出现了进位,使尾数成为10.xxxx或11.xxxx的形式。例如, 10.0011 ∗ 2 5 10.0011*2^5 10.001125右规一位后便成为 1.00011 ∗ 2 6 1.00011*2^6 1.0001126的规格化形式了。

3.4 舍入处理

浮点运算在对阶或右规时,尾数需要右移,被右移出去的位会被丢掉,从而造成运算结果精度的损失。为了减少这种精度损失,可以将一定位数的移出位先保留起来,称为保护位,在规格化后用于舍入处理。
IEEE754标准列出了四种可选的舍入处理方法:
(1)就近舍入(round to nearest)这是标准列出的默认舍入方式,其含义相当于我们日常所说的“四舍五入”。例如,对于32位单精度浮点数来说,若超出可保存的23位的多余位大于等于100…01,则多余位的值超过了最低可表示位值的一半,这种情况下,舍入的方法是在尾数的最低有效位上加1;若多余位小于等于011…11,则直接舍去;若多余位为100…00,此时再判断尾数的最低有效位的值,若为0则直接舍去,若为1则再加1。
(2)朝+∞舍入(round toward +∞)对正数来说,只要多余位不为全0,则向尾数最低有效位进1;对负数来说,则是简单地舍去。
(3)朝-∞舍入(round toward -∞)与朝+∞舍入方法正好相反,对正数来说,只是简单地舍去;对负数来说,只要多余位不为全0,则向尾数最低有效位进1。
(4)朝0舍入(round toward 0)
即简单地截断舍去,而不管多余位是什么值。这种方法实现简单,但容易形成累积误差,且舍入处理后的值总是向下偏差。

3.5 溢出判断

与定点数运算不同的是,浮点数的溢出是以其运算结果的阶码的值是否产生溢出来判断的。若阶码的值超过了阶码所能表示的最大正数,则为上溢,进一步,若此时浮点数为正数,则为正上溢,记为+∞,若浮点数为负数,则为负上溢,记为-∞;若阶码的值超过了阶码所能表示的最小负数,则为下溢,进一步,若此时浮点数为正数,则为正下溢,若浮点数为负数,则为负下溢。正下溢和负下溢都作为0处理。
要注意的是,浮点数的表示范围和补码表示的定点数的表示范围是有所不同的,定点数的表示范围是连续的,而浮点数的表示范围可能是不连续的。

3.6 解决疑问

知道了这些,对于0.05f + 0.01f=0.060000002f,我们可以得到整个计算过程如下:
float a=0.05;b=0.01;
a = ( 0.05 ) 10 = ( 00111101010011001100110011001101 ) 2 a=(0.05)_{10}=(0011 1101 0100 1100 1100 1100 1100 1101)_2 a=(0.05)10=(00111101010011001100110011001101)2 S a = 0 S_a=0 Sa=0 E a = 01111010 E_a=011 1101 0 Ea=01111010 M a = 1.10011001100110011001101 M_a=1.100 1100 1100 1100 1100 1101 Ma=1.10011001100110011001101
b = ( 0.01 ) 10 = ( 00111100001000111101011100001010 ) 2 b=(0.01)_{10}=(0011 1100 0010 0011 1101 0111 0000 1010)_2 b=(0.01)10=(00111100001000111101011100001010)2 S b = 0 S_b=0 Sb=0 E b = 01111000 E_b=011 1100 0 Eb=01111000 M b = 1.01000111101011100001010 M_b=1.010 0011 1101 0111 0000 1010 Mb=1.01000111101011100001010
a+b=?
第一步:对阶
E b < E a E_b<E_a Eb<Ea E a − E b = 2 E_a-E_b=2 EaEb=2
M b M_b Mb要调整为0.01010 0011 1101 0111 0000 10 10
E=011 1101 0
第二步:尾数运算
     1.100 1100 1100 1100 1100 1101
+ 0.010 1000 1111 0101 1100 0010
     1.111 0101 1100 0010 1000 1111
第三步:规格化
1.111 0101 1100 0010 1000 1111‬已经是个规格化数据了
第四步:舍入处理
由于在对阶时, M b M_b Mb有右移,保护位为10,按照上述规则,尾数的最低有效位加1,得到1.111 0101 1100 0010 1001 0000
第五步:溢出判断
没有溢出,阶码不调整,所以最后的结果为
a+b=(0 01111010 11101011100001010010000) 2 _2 2=(0 011 1101 0111 0101 1100 0010 1001 0000) 2 _2 2=(0.0600000022) 10 _{10} 10

参考:
计算机中浮点数的二进制表示
浮点数的运算步骤
float存储方式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值