美文网首页
浮点数解密

浮点数解密

作者: 土豆肉丝盖浇饭 | 来源:发表于2020-02-24 21:33 被阅读0次

    本文不仅仅适用于java

    问题

    先上两段代码,经过本文的讲解后,你应该能深刻的理解浮点数并且可能不想用"浮点数"(float/double)了

    问题1

            float a = 0.1f;
            float b = 0.2f;
            float c = a +b;
            System.out.println(Float.compare(0.3f,a+b));
            System.out.println(c-0.3f);
    
            System.out.println("===================");
    
            double a1 = 0.1;
            double b1 = 0.2;
            double c1 =a1 +b1;
            System.out.println(Double.compare(0.3d,c1));
            System.out.println(c1 -0.3);
            System.out.println(Math.pow(2,-54));
    

    输出

    true
    0.0
    ===================
    false
    5.551115123125783E-17
    5.551115123125783E-17
    

    为什么float 的0.3相等,而double的0.3不想等,相差了Math.pow(2,-54)

    问题2

            float x = 0.0f/0.0f;
            System.out.println(x);
    

    输出

    NaN
    

    为什么输出的是NaN,而不是报java.lang.ArithmeticException: / by zero这个异常

    IEEE 754

    我们知道任何数据在内存中都是以二进制存储的,所以IEEE 754 规定了浮点数在计算机中二进制的存储方式,java语言也遵守这一标准(哪个流行语言敢不遵守)。

    浮点数类型

    单/双精度浮点数分别对应java语言中的float和double

    单精度浮点数

    双精度浮点数

    可以从上面的图可以看出,在 IEEE 754标准下,浮点数的二进制格式分为三部分,类似于科学计数法。

    S代表符号位,0代表正,1代表负
    E代表指数,b代表指数有多少位
    M为尾数,n代表尾数有多少位

    二进制数转换为浮点数

    从二进制数转换为浮点数的公式如下

    上面的公式有2个点,大家可能不理解,但是它的设计巧妙十分巧妙

    第一,为什么E需要减去一个掩码的大小?因为E生成的时候加上了一个掩码的大小。

    那么,为什么要加上一个掩码的大小呢?
    首先我们的指数是有正负的,所以E的最高位为符号位,以8位的E为例子,使用使用普通的符号数,它能表示数字范围为 +0 ~ 127,-0~ -127,而对于一个数来当指数来讲,我们不需要2个0。所以通过加上一个掩码的大小(专业术语叫做移位存储)来解决这个问题。通过移位存储,我们能表达的数字范围为0 ~ 128,0~ -127 。

    移位存储你也可以理解为用无符号数来映射有符号数

    第二,为什么M前需要加上1?因为尾数M省略了一位。经过移位后,第一位强制为1,所以省略了。n位的尾数实际上是n+1位。
    第一位强制为1?那不是不可能代表0了,浮点数的0怎么表示?这个下面再讲。

    浮点数转换为二进制数

    整数部分,除以2,不断取余数的1和0,结果倒序排列
    小数部分,乘以2,不断取整数的1和0,结果正序排列

    以10.5,10.2 ,0.2 和 0.5举例子
    对于整数部分的10来讲,我们可以得到它的二进制为1010
    对于0.5来讲

    0.5 * 2 = 1.0 取1
    

    所以0.5的二进制为.1

    对于0.2来讲

    0.2 * 2 = 0.4 取0
    0.4 * 2 = 0.8 取0
    0.8 * 2 = 1.6 取1
    0.6 * 2 = 1.2 取1
    0.2 * 2 = 0.4 取0
    循环下去
    

    所以0.2的二进制为 .001 1001 1001 1001...

    规范起见,我们使用到的浮点数二进制需要通过幂次进位保证整形部分必须为1

    以32位的单精度浮点数举例

    10.5 的二进制表示为 0 10000010 0101 0000 0000 0000 0000 000
    10.2 的二进制表示为 0 10000010 0100 0110 0110 0110 0110 011
    0.5的二进制表示为 0 01111110 0000 0000 0000 0000 0000 000
    0.2的二进制表示为 0 01111100 1001 1001 1001 1001 1001 101

    转换代码

    人肉转换太痛苦了,上代码

        public static void main(String[] args) {
    
            getFloatBin(0.1f);
            getFloatBin(0.2f);
            getFloatBin(0.3f);
    
            getDoubleBin(0.1d);
            getDoubleBin(0.2d);
            getDoubleBin(0.3d);
        }
    
        public static void getFloatBin(float f){
            int b=Float.floatToIntBits(f);
            String s = Integer.toBinaryString(b);
            int size = s.length();
            for(int i =0 ;i < 32-size;i++){
                s = "0" +s;
            }
            for(int i =0 ;i< s.length();i++){
                if(i ==1 || i ==9){
                    System.out.print(",");
                }
                if(i >8 && (i-9)%4 ==0){
                    System.out.print(" ");
                }
                System.out.print(s.charAt(i));
            }
            System.out.println();
    
        }
    
        public static void getDoubleBin(double d){
            long l = Double.doubleToLongBits(d);
            String s = Long.toBinaryString(l);
            int size = s.length();
            for(int i =0 ;i < 64-size;i++){
                s = "0" +s;
            }
            for(int i =0 ;i< s.length();i++){
                if(i ==1 || i ==12){
                    System.out.print(",");
                }
                if(i >12 && (i-12)%4 ==0){
                    System.out.print(" ");
                }
                System.out.print(s.charAt(i));
            }
            System.out.println();
        }
    

    可以看出一个规律,只要不是5结尾的浮点数,对应的二进制都是无限循环,必须做舍入,二进制的舍入很简单,只要下一位是1,就进位

    特殊数值处理

    上面讲到指数的E的取值范围是0 ~ 128,0~ -127,但是 128(11111111) 和 -127(00000000) 这两个指数对应的浮点数有特殊的含义。

    +0

    -0

    +∞

    -∞

    NAN

    NAN = not a number ,浮点数的除以0操作会返回这个


    非规范化数

    除了+/-0以外,E=00000000的浮点数用于表示一些很小,很接近于0的数字,由于这个数省略了0开头所以叫非规范化数,对应省略1开头的叫规范化数。知道有这个存在即可,基本用不上。

    浮点数的范围

    知道了浮点数的二进制格式,那么它的范围也是可以轻松推出来的

    规范化-最小值-绝对值

    00 80 00 00 = 2-126 * (1+0/223)= 2-126 ≈ 1,17549435∙e-38
    80 80 00 00 = -2-126 * (1+0/223)=-2-126≈ -1,17549435∙e-38

    规范化-最大值-绝对值

    7F 7F FF FF = 2127(2-2-23) = 2128≈ 3,40282347∙e+38
    FF 7F FF FF = -2127
    (2-2-23) = -2128≈ -3,40282347∙e+38

    非规范化浮点数就不列了,自行研究

    解答开篇问题

    问题1

    对于单精度浮点数,也就是float,0.1+0.2计算逻辑如下

    0.1 = 0,01111100,1001 1001 1001 1001 1001 101
        = 1,1001 1001 1001 1001 1001 101  * 2^-4
    0.2 = 0,01111100,1001 1001 1001 1001 1001 101
        = 1,1001 1001 1001 1001 1001 101  * 2^-3
    0.1 + 0.2 =  
                 1,1001 1001 1001 1001 1001 101 * 2^-4 +
                11,0011 0011 0011 0011 0011 01 * 2^-4
              = 100,1100 1100 1100 1100 1100 111 * 2^-4
              =  1,0011 0011 0011 0011 0011 001(1) * 2^-2
              =  1,0011 0011 0011 0011 0011 010 * 2^-2
    0.3 = 0,01111101,0011 0011 0011 0011 0011 010 
        = 1, 0011 0011 0011 0011 0011 010 * 2^-2
    

    对于双精度浮点数,也就是double类型,计算逻辑如下

    0.1 = 0,01111111011,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
          = 1,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 * 2 ^ -4
    0.2 = 0,01111111100,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
          = 1,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010  * 2 ^ -3
    0.1 + 0.2 = 
            1,1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 * 2 ^ -4 +
           11,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010 * 2 ^ -4
        = 100,1100 1100 1100 1100 1100 1100 1100 1100 1100 1110 1100 1100 1110 * 2 ^ -4
        =   1,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 (10)  * 2 ^ -2
        =   1,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100  * 2 ^ -2
    
    0.3 = 0,01111111101,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011
          = 1,0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011  * 2 ^ -2
    

    浮点数转换为二进制后,存在无限循环的问题,因此到达有效位后会进行舍入,对于二进制数是1进位0不进位的规则。

    在32位浮点数的 0.2 +0.1中,0.1,0.2,0.1+0.2,0.3,都是存在舍入操作的并且进位一样多。
    而在64位浮点数的0.2+0.1中,由于有效数字是52位,刚好是4的倍数,并且后面接的二进制0011不会产生进位,但是在0.1 + 0.2 的过程中由于指数不对齐产生了进位,才导致0.1+0.2大于了0.3。

    问题2

    java语言的浮点数实现是遵循了IEEE 754规范,而NAN也是这个规范的要求之一。所以对于浮点数的除以0操作是会返回NAN的。

    如何在java程序中处理浮点数

    经过上面的分析,显而易见,如果你在java应用中使用float 或者 double类型进行运算,肯定是会存在精度问题的。

    所以在应用中关于金额的字段务必使用BigDecimal或者使用整型进行操作!!!

    如果不进行运算操作,还是可以照常使用的

    至于BigDecimal的原理就是,整型在转化为二进制的时候不会存在精度问题。

    参考

    IEEE 754 标准
    IEEE 754浮点数标准详解
    为什么说浮点数缺乏精确性? python中浮点数运算问题
    浮点数在内存中的表示移位存储难点的理解

    相关文章

      网友评论

          本文标题:浮点数解密

          本文链接:https://www.haomeiwen.com/subject/lresqhtx.html