计算机基础-浮点数的表示

利用Java的大数展示浮点数之谜

引用知乎:https://zhuanlan.zhihu.com/p/89320102

1.浮点数的存储格式

浮点数的精度问题并不只是存在于某个语言之上,而是在整个计算机体系上来说都存在这样的问题

现在几乎所有语言都支持 IEEE 754 的二进制浮点格式。在说明这个格式之前,先看看科学计数法,有一个这样的数:123.456,表示成科学计数法是:+ 1.23456 * 10^2,这个表示法有三个部分:

  • +符号
  • 3.27849有效数
  • 10的2次方上的2指数

二进制也有一样的科学计数法,如:

1001.0101  可以表示成 + 1.0010101 * 2^3
0.001011 可以表示成 + 1.011 * 2^-3

方法是将小数点移动到最左边的1之后,向左移多少位,就是2的几次方;向右移多少位,就是2的负几次方。

IEEE 754称这种表示方法叫正规化,这种叫正规数,其也规定了一种叫非正规数

  • +为符号
  • 1.0010101为有效数
  • 3 为指数

IEEE 754的浮点数格式遵循科学计数法的表现方式,以32位浮点数为例(下面默认都是32位浮点数):

  • 第31位表示符号,简写为S,0为正,1为负
  • 接下来的8位与指数有关,简写为E,因为是8位,所以E的范围是[0, 255],其中0和255代表特殊值,剩下的值减去127得到指数:E - 127,即指数的范围是[-126, 127]
  • 剩下的23位表示有效数的小数部分,也叫做尾数,我们简写为M,真正的有效数是 1.M,注意前面的1是被省略了的,正规化表示的数的M总是位于[0.5,1)这个区间之内

下面地公式是IEEE 754单精度浮点数地表示方法

(-1)^S * 1.M * 2^(E-127)

以上面图中二进制为例,一步步计算最终的值:

// E等于十进制的124
E = ‭01111100‬ = 124
// 代入公式
(-1)^0 * 1.01000000000000000000000 * 2^(124 - 127)
// 继续
1 * 1.01 * 2^-3
// 继续:下面就是二进制的最终值
0.00101
// 继续
0 + 0/2 + 0/4 + 1/8 + 0/16 + 1/32
// 继续
0 + 0 + 0 + 0.125 + 0 + 0.03125
// 得到结果
0.15625‬

2.浮点数越大,精度越差

现在我们假设一个问题,我们假设现在E的值固定是126,S的值固定是0,那么这个浮点数最终能表示的数的范围是多少呢?由于E和S值是固定的,剩下的能决定浮点数的值的就是M了,因为M有23位,算上省略的一位也就是24位,那么最终这个浮点数的范围就是 [0.5,1),也就是说,我们可以用2^24个数来表示[0.5,1)这个区间的数(首位固定是1),我们把这段区间想象成一个线段,每个数这2^24个数就是这个线段上的点,是不是能想到点什么?精度由谁决定?当然是由点的密集程度决定了,点越多是不是能表示的数也就越精确?

我们现在又来假设,假设E是127,S是0,那么最终这个浮点数的表示范围就成了[1,2),这时候你再看,我们的M同样是2^24次方个数,但是要表示的数的区间却变大了,是不是点之间的空隙也就变大了?精度也就随之变差了?所以说,其实一个浮点数越大,随之带来的就是精度的变差。

 

3.动手测试一下

我们假设现在想表示0.7这个单精度浮点数,把他转换为二进制形式可以发现是不能精确的表示出0.7的,其会一直出现循环。所以说,我们在把0.7存储到计算机的时候,其实就已经出现了精度损失。可能你问题由来了,那为什么我存一个0.7,明明精度出现了问题,但是还是能正确的表示出0.7?且听我慢慢道来

public static void main(String[] args) {
    float a = 0.7f;
    float b = 0.69999996f;
    float c = 0.70000001f;

    System.out.println(a);  //0.7
    System.out.println(b);  //0.7
    System.out.println(c);  //0.7

    System.out.println(Integer.toBinaryString(Float.floatToIntBits(-a))); //10111111001100110011001100110011
    System.out.println(Integer.toBinaryString(Float.floatToIntBits(-b))); //10111111001100110011001100110011
    System.out.println(Integer.toBinaryString(Float.floatToIntBits(-c))); //10111111001100110011001100110011

    System.out.println(a == b); //true
    System.out.println(a == c); //true


    float d = 0.70000002f;
    System.out.println(d);    //0.70000005
    System.out.println(Integer.toUnsignedString(Float.floatToIntBits(d), 16));//3f333334

}

为什么会出现上面的情况?因为在将0.69999996f或者0.7f转换为二进制的时候,因为精度问题,其实最终转换成的二进制都是一样的,看上面输出的二进制也能看出来,那为什么就输出了0.7呢?“10111111001100110011001100110011”我们取出这个数中的M的部分,也就是“1.01100110011001100110011”,E的值为“-1”,最终我们套入上面的公式计算一下看计算出来的结果到底是多少,利用Java的大数来计算一下

BigDecimal bigDecimal = new BigDecimal("0.25");
BigDecimal sum = new BigDecimal("0.5");
char[] chars = Integer.toBinaryString(Float.floatToIntBits(-c)).substring(8).toCharArray();
for (int i = 1; i < 24; i++) {
    if (chars[i] == '1') {
        sum = sum.add(bigDecimal);
    }
    bigDecimal = bigDecimal.divide(new BigDecimal(2));
}
System.out.println(sum.toPlainString()); //0.699999988079071044921875

其实最终的结果,也不是0.7,那为什么就能正确的输出0.7呢?其实啊这只是我们的计算过程,在计算机里面,还原0.7这个数的二进制的时候,也不会像我们这么精确,因为他表示不了啊。他最多只能看到这么多“0.6999999”,那是不是就是输出0.699999了呢?其实不是的,这时候后面的8就起到关键作用了,因为0.6999998这个数离0.7更接近,所以,最终就给我们输出了0.7,如果你的数是0.69999994,那最终的可能就是0.69999999了,具体过程是怎么实现的?说实话我也不知道,我猜想应该是CPU中的硬件完成的,因为CUP在计算的时候如果出现数字越界,有时候并不是直接抛弃,其有自己的处理规则。

4.特殊的浮点数值

有一些存储格式被预定义为特殊值,它们是:

0值

0:当E=0,M=0时,浮点数表示0,因为S的存在,浮点数会出现两个0的存储格式(以单精度为例):

+0 = 0 00000000 00000000000000000000000
-0 = 1 00000000 00000000000000000000000

无穷(Infinity)

当E=255,M=0时,表示无穷,同样有正无穷和负无穷:

+INFINITY = 0 11111111 00000000000000000000000
-INFINITY = 1 11111111 00000000000000000000000

无穷数有这样一些性质:

  • 任何有穷数加上无穷,还是等于无穷,如:
var f = 1000.0 + math.Inf(1)
fmt.Printf("%f\n", f)       // => +Inf
  • 任何正有穷数乘以无穷,还是等于无穷;任何负有穷数乘以无穷,等于符号相反的无穷。
  • 任何有穷数除以无穷,等于0。

总之,无穷数就是大到无法正常计算结果的数。

非数(NaN)

当E=255,M != 0时,称为非数:

0 11111111 00000000000000000000000 ~ 0 11111111 11111111111111111111111
1 11111111 00000000000000000000000 ~ 1 11111111 11111111111111111111111

非数也有很多性质,不过最值得提出来的是 NaN != NaN :)

Subnormals

前面说到浮点数的有效数是1.M,这种形式叫正规数。

当E=0,M!=0时,浮点数处于Subnormal的范围,它的表示方法是0.M,即前面的数是0,这种数也叫非正规数(denormal)。

定义非正规数的目的是:使最小正规数和0之间更平滑,换句话说能存储更多很小的数,比如下面的数:

0.00110001101001 * 2^(−126)

如果用正规的表示方式是:1.10001101001 * 2^(-129),指数部分已经超过E能表示的范围,所以按正规方式是没有办法存储的。但按非正规方式就可以:

  • E = 0
  • M = 00110001101001
  • 公式写成 (-1)^S * 0.M * 2^(E-126)

这样就能表达很小的数了,不过这些数的精度要比正规数低,相当于牺牲精度为代价,消除最小正规数和0之间的差距。

作为程序员,特别是服务端程序员,我们只要明白浮点数不是均匀分布的,越大的浮点数精度越低,大多数实数在计算机中只能以近似值表示就差不多了。剩下的让浮点运算单元帮我们处理吧:)

发布了162 篇原创文章 · 获赞 44 · 访问量 8840

猜你喜欢

转载自blog.csdn.net/P19777/article/details/103648385