延伸阅读-浮点数二进制存储


1. 浮点数的二进制存储

浮点数相对比较复杂:它由三部分组成:符号位,指数位,小数部分

例如对于float来讲:

符号位1位,以s表示,取值 (-1)^s

指数部分8位,以E表示,取值 -126~127,存储时在真正指数值的基础上+127

小数部分23位,以M表示,1<=M<2 。 采用科学计数法表示的二进制小数,因为M总是1.xxxx,所以保存时只存储小数部分,例如1.01只存储小数部分的01,整数部分读取时再加上,这样的目的可以多存储一位数,即24位。

比如:9.5 的二进制为1001.1,采用科学计数法后为1.0011 * 2^3

符号位和小数比较简单

指数部分存储规则:

  0000 0011 (十进制3)
  0111 1111 (十进制127)
+ -------------------------
  1000 0010 (十进制-126)

最终的存储格式为:

符号位 指数 小数
0 1000 0010 001 1000 0000 0000 0000 0000

2. 浮点数精度问题

以小数部分的精度为例:刚才分析过小数部分能够表示的最大范围是24位,换算成十进制就是2^24=16777216

看看这个数值前后在存储时有什么问题:

实际数 float表示的浮点数 二进制存储
16777211 1.6777211E7 0100 1011 0111 1111 1111 1111 1111 1011
16777212 1.6777212E7 0100 1011 0111 1111 1111 1111 1111 1100
16777213 1.6777213E7 0100 1011 0111 1111 1111 1111 1111 1101
16777214 1.6777214E7 0100 1011 0111 1111 1111 1111 1111 1110
16777215 1.6777215E7 0100 1011 0111 1111 1111 1111 1111 1111
16777216 1.6777216E7 0100 1011 1000 0000 0000 0000 0000 0000
16777217 1.6777216E7 0100 1011 1000 0000 0000 0000 0000 0000
16777218 1.6777218E7 0100 1011 1000 0000 0000 0000 0000 0001
16777219 1.677722E7 0100 1011 1000 0000 0000 0000 0000 0010
16777220 1.677722E7 0100 1011 1000 0000 0000 0000 0000 0010
16777221 1.677722E7 0100 1011 1000 0000 0000 0000 0000 0010

可以看到16777215 之前的数据没有问题,注意到 16777216 与 16777217在存储时值已经相同;类似的,16777219、16777220、16777221三个数在存储时也是相同的。

换句话说,这时候计算机已经无法区分它们了,可以验证:

System.out.println(16777216f == 16777217f); // true
System.out.println(16777220f == 16777219f); // true
System.out.println(16777220f == 16777221f); // true

结论:float的小数部分只能够精确到2^23,也就是小于16777216 的数字能够保证小数点后7位都是有效的,大于等于它的数只能保证小数点后6位是有效的。

3. 浮点数的运算误差

前面的精度,是指浮点数在存储时发生的。此外浮点数在运算时也会有一定问题,下面来详细分析。

运算 结果
2.0f-1.5f 0.5
2.0f-1.4f 0.6
2.0f-1.3f 0.70000005
2.0f-1.2f 0.79999995
2.0f-1.1f 0.9
12.0f-11.9f 0.10000038

看到以上结果,霎时间感觉不好了:连简单的小数减法都算不对!

来分析一下过程吧,主要分析小数部分:

2.0f-1.5f

0100 0000 0000 0000 0000 0000 0000 0000  (2.0)
0011 1111 1100 0000 0000 0000 0000 0000  (1.5)

提取小数部分,并对齐
1.00000000000000000000000 (2.0 从存储区域取后23位,前面补一个1.0) 
0.110000000000000000000000 (1.5 从存储区域取后23位,前面补一个1.0,小数点要左移一位,保证与2.0对齐)

减法
0.010000000000000000000000

采用1.xxxx科学计数法表示,小数保留23位(小数点向右移了2位,补了一个0)
1.000 0000 0000 0000 0000 0000

对比真正0.5
0011 1111 0000 0000 0000 0000 0000 0000 (真的0.5 )
xxxx xxxx x000 0000 0000 0000 0000 0000 (我们的2.0-1.5)

发现它们的小数部分都是一致的,没有误差

2.0f-1.4f

0100 0000 0000 0000 0000 0000 0000 0000  (2.0)
0011 1111 1011 0011 0011 0011 0011 0011  (1.4)

提取小数部分,并对齐
1.00000000000000000000000 (2.0 从存储区域取后23位,前面补一个1.0) 
0.101100110011001100110011 (1.4 从存储区域取后23位,前面补一个1.0,小数点要左移一位,保证与2.0对齐)

减法
0.010011001100110011001101

采用1.xxxx科学计数法表示,小数保留23位(小数点向右移了2位,补了一个0)
1.001 1001 1001 1001 1001 1010

对比真正0.6
0011 1111 0001 1001 1001 1001 1001 1010 (真的0.6 )
xxxx xxxx x001 1001 1001 1001 1001 1010 (我们的2.0-1.4)

发现它们的小数部分都是一致的,没有误差

2.0f-1.3f

0100 0000 0000 0000 0000 0000 0000 0000  (2.0)
0011 1111 1010 0110 0110 0110 0110 0110  (1.3)

提取小数部分,并对齐
1.00000000000000000000000 (2.0 从存储区域取后23位,前面补一个1.0) 
0.101001100110011001100110 (1.3 从存储区域取后23位,前面补一个1.0,小数点要左移一位,保证与2.0对齐)

减法
0.010110011001100110011001

采用1.xxxx科学计数法表示,小数保留23位(小数点向右移了2位,补了一个0)
1.011 0011 0011 0011 0011 0010

对比真正0.7
0011 1111 0011 0011 0011 0011 0011 0011 (真的0.7 )
xxxx xxxx x011 0011 0011 0011 0011 0010 (我们的2.0-1.3)

发现最后一位不一样,失之毫厘,谬以千里,问题就出在保留23位小数结果时,在最末尾补0,但不是所有情况补0都正确!

2.0f-1.2f

0100 0000 0000 0000 0000 0000 0000 0000  (2.0)
0011 1111 1001 1001 1001 1001 1001 1010  (1.2)

提取小数部分,并对齐
1.00000000000000000000000 (2.0 从存储区域取后23位,前面补一个1.0) 
0.100110011001100110011010 (1.2 从存储区域取后23位,前面补一个1.0,小数点要左移一位,保证与2.0对齐)

减法
0.011001100110011001100110

采用1.xxxx科学计数法表示,小数保留23位(小数点向右移了2位,补了一个0)
1.100 1100 1100 1100 1100 1100

对比真正0.8
0011 1111 0100 1100 1100 1100 1100 1101 (真的0.8 )
xxxx xxxx x100 1100 1100 1100 1100 1100 (我们的2.0-1.2)

发现最后一位不一样,失之毫厘,谬以千里,问题就出在保留23位小数结果时,在最末尾补0,但不是所有情况补0都正确!

12.0f-11.9f

0100 0001 0100 0000 0000 0000 0000 0000  (12.0)
0100 0001 0011 1110 0110 0110 0110 0110  (11.9)

提取小数部分,并对齐
1.10000000000000000000000 (2.0 从存储区域取后23位,前面补一个1.0) 
1.01111100110011001100110 (1.2 从存储区域取后23位,前面补一个1.0)

减法
0.00000011001100110011010

采用1.xxxx科学计数法表示,小数保留23位(小数点向左移了7位,补了7个0)
1.100 1100 1100 1101 0000 0000


对比真正0.1
0011 1101 1100 1100 1100 1100 1100 1101 (真的0.1 )
xxxx xxxx x100 1100 1100 1101 0000 0000 (我们的12.0-11.9)

这回差的有点大,因为我们补了7个0

附:将float中的每个bit打印出来的工具方法:

private static void print(float a) {
    String s = String.format("%32s", Integer.toBinaryString(Float.floatToIntBits(a))).replaceAll("\\s","0");
    StringBuilder sb = new StringBuilder(32+8);
    for(int i = 0; i<s.length();i++) {
        sb.append(s.charAt(i));
        if((i+1) % 4 == 0){
            sb.append(" ");
        }
    }
    System.out.println(sb.toString());
}

results matching ""

    No results matching ""