float与double的精度问题

  【问题】

  在之前的一篇文章中,提到过float和double不能用于金额计算,原因是浮点型数据计算中会产生误差,造成结果不准确。这一篇我们仔细分析这种误差的产生来源。

  先看一段代码:

    public static void main(String[] args) {
        float a = 34.12f;
        float b = 34;
        float c = 0.12f;
        System.out.println(a - b);
        System.out.println(c);
    }
  //0.11999893
  //0.12

  问题来了:为什么计算出来的0.12不能准确地展示,但是浮点型的0.12可以完整展示出来呢?如果将三个变量换为double类型,第一个结果会变成:0.11999999999999744,这又是为什么?

————————————————————————————————————————————————

  【拆解】

  首先,我们来了解,float和double这两种浮点类型的数据,在计算中是怎么存储的。比如22.45这个浮点数,在计算机的0和1这个体系里是怎么存储呢?

  整数部分22,直接转换成二进制:10110

  小数部分0.45,计算机的处理方式是乘以2,并取整数:

0.45*2=0.90---0
0.90*2=1.80---1
0.80*2=1.60---1
0.60*2=1.20---1
0.20*2=0.40---0
0.40*2=0.80---0
0.80*2=1.60---1
......

  此时,我们已经知道,这种方式去存储小数的话,一定是一个需要无限空间的,计算机不可能也没必要为了这么一个浮点数,进行不限制空间的精确存储,肯定是有舍弃的,这也就是32位和64位的精度这个说法的来源。那么,此时还有一个问题,这个小数点该如何处理呢?

  到这里,我们就要了解计算机存储浮点数的规则。这个规则定义了如何处理符号,小数点和精度。比如22.45这个数,我们刚刚转换的结果为:10110.01110011001100......,那么计算机要怎么存储这个数字呢?

  首先,计算机使用科学计数法,将10110.01110011001100...表示为1.0110...*2^4,这里涉及到了原始的数据、指数、符号三个关键因素。也就是说,只要确定了这三个问题,那么浮点数就可以完全按照0和1的方式存储下来。

————————————————————————————————————————————————

  【背景】

  其实在20世纪80年代之前,业界还没有一个统一的浮点数表示标准。很多计算机制造商根据自己的需要来设计自己的浮点数表示规则,以及浮点数的执行运算细节,这样就给代码的可移植性造成了重大障碍。

  直到 1976 年,Intel 公司打算为其 8086 微处理器引进一种浮点数协处理器时,意识到作为芯片设计者的电子工程师和固体物理学家也许并不能通过数值分析来选择最合理的浮点数二进制格式。于是,他们邀请加州大学伯克利分校的 William Kahan 教授(当时最优秀的数值分析家)来为 8087 浮点处理器(FPU)设计浮点数格式。而这时,William Kahan 教授又找来两个专家协助他,于是就有了 KCS 组合(Kahn、Coonan和Stone),并共同完成了 Intel 公司的浮点数格式设计。

  由于 Intel 公司的 KCS 浮点数格式完成得如此出色,以致 IEEE(Institute of Electrical and Electronics Engineers,电子电气工程师协会)决定采用一个非常接近 KCS 的方案作为 IEEE 的标准浮点格式。于是,IEEE 于 1985 年制订了二进制浮点运算标准 IEEE 754(IEEE Standard for Binary Floating-Point Arithmetic,ANSI/IEEE Std 754-1985),该标准限定指数的底为 2,并于同年被美国引用为 ANSI 标准。目前,几乎所有的计算机都支持 IEEE 754 标准,它大大地改善了科学应用程序的可移植性。

  考虑到 IBM System/370 的影响,IEEE 于 1987 年推出了与底数无关的二进制浮点运算标准 IEEE 854,并于同年被美国引用为 ANSI 标准。1989 年,国际标准组织 IEC 批准 IEEE 754/854 为国际标准 IEC 559:1989。后来经修订后,标准号改为 IEC 60559。现在,几乎所有的浮点处理器完全或基本支持 IEC 60559。同时,C99 的浮点运算也支持 IEC 60559。

  IEEE 浮点数标准是从逻辑上用三元组{S,E,M}来表示一个数 V 的,即 V=(-1)S×M×2E,如图所示。

float与double的精度问题

————————————————————————————————————————————————

  【规则】

  我们来详细了解double与float的组成规则。

  如图所示,double类型中,sign用1bit表示正负,其中0表示正,1表示负;exponent用11bits表示科学计数法中的指数数据;剩余的52bits表示尾数,称为R64.53标准。float类型中,sign用1bit表示正负,其中0表示正,1表示负;exponent用8bits表示科学计数法中的指数数据;剩余的23bits表示尾数,称为R32.24标准。

  至此,我们应该明白,绝大部分浮点数,在存储时都会损失部分数据,即存储之后,再将存储数据转换为原始数据时会产生误差。而double和float只是保留尾数长度不同,所以精度不同。

  回到文初的问题,整个过程应该是这样的:

    计算机先存储a,此时精度不会丢失;存储b时,精度丢失;二进制的a减去二进制的b,过程(求阶差、対阶、尾数相减、规格化)在此不详细描述;最终计算机再将这个二进制数按照IEEE规则转回去,输出;

    第二步的c,计算机转化为二进制存储,再按照IEEE规则转回去。

  虽然第二步的精度也有损失,但是精度的损失保持在很小的范围,所以从二进制转为字符串展示时,能够保持字面一致;但是第一步在计算过程中损失了更多的精度,字面的一致已经无法保证。说到底,这是一个精度损失了多少的问题,如果是在一个极小范围内的精度损失,即便两个浮点数值略微不同,但由于最终转换为的二进制保持了一致,所以再转回字符串时可以保持字面相同,我们再看一个示例:

        System.out.println(Long.toBinaryString(Float.floatToIntBits(22.45999999999999999999f))+"----"+22.45999999999999999999f);
        System.out.println(Long.toBinaryString(Float.floatToIntBits(22.46000000000000000001f))+"----"+22.46000000000000000001f);
        System.out.println(Long.toBinaryString(Float.floatToIntBits(22.45999999999999999998f))+"----"+22.45999999999999999998f);
        System.out.println(Long.toBinaryString(Float.floatToIntBits(22.45999999999999999997f))+"----"+22.45999999999999999997f);
        System.out.println(Long.toBinaryString(Float.floatToIntBits(22.45999799999999999999f))+"----"+22.45999799999999999999f);

  结果为:

1000001101100111010111000010100----22.46
1000001101100111010111000010100----22.46
1000001101100111010111000010100----22.46
1000001101100111010111000010100----22.46
1000001101100111010111000010011----22.459997

  可以看到,前四个数据,虽然有所不同,但是极为接近,导致按照R32.24标准转化的二进制相同。所以最终转化的字面值也是相同的。但是最后一个数据按照标准转为二进制已经发生了变化,所以再转回字面值也发生了变化。

————————————————————————————————————————————————

  【拓展】

  此时我们已经了解了float和double的误差来源。那么BigDecimal为什么可以保证计算精度呢?  

  此处直接给出原因:BigDecimal并没有按照浮点数那样,依照IEEE754标准进行转换,而是直接将浮点数放大一定的倍数,使得小数刚好转换为整数,再进行整数转换二进制,也就不会出现精度损失。也就是说,在BigDecimal处理数据的过程中,不会出现无限循环的情况。

  但如果在BigDecimal的除法运算中,没有指定scale,造成了循环的除法运算,会抛出异常:

Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    at java.math.BigDecimal.divide(BigDecimal.java:1690)

 

 

 

 

 

 

 

 

 

 

 

 

 

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

float与double的精度问题

上一篇:Redis


下一篇:K8S常用命令