浮点数加法运算的困惑

缘起:

公司一小姑娘群发邮件求助,邮件内容是关于浮点数之间运算结果莫名其妙的多了一些小数位,小姑娘想不通所以想问问有人知道原因不?涉及的代码是下面的样子

function addTwoFloatString(stra , strb){
    return parseFloat(stra) + parseFloat(strb);
}

addTwoFloatString('2222222','269567.53');
->2491789.5300000003

大家看到这个问题后献计献策,有说不应该那样,应该这样

Number( 123.5678.toFixed(2) )
还有说应该用BigDecimal做浮点数相加运算的。

看到这个求助后,直觉告诉我这个和浮点数的表示法肯定有关系,至于为什么,详细的答案是真说不好。如果非要搪塞一下,我会说浮点数之间的运算会有精度问题,如果深究为什么,那就糗了。

验证

首先看看是否其他语言中也存在类似的问题,ok,java中也存在同样的问题,那么基本可以断定这个现象和编程语言关系不大。

接着需要确认的问题是,JavaScript中浮点数是如何定义的:

JavaScript中的浮点数采用IEEE-754格式的规定。更具体的说是一个双精度格式,这意味着每个浮点数占64位。虽然它不是二进制表示浮点数的唯一途径,但它是目前最广泛使用的格式,另外,JavaScript中的数值不区分整数值和浮点数值,所有数字都采用的是64位浮点数表示法。

似乎看到了希望,IEEE754

wiki中对IEEE754中64位浮点数有下面的描述,总长度占用64位,其中1为符号位,标明浮点数是正数还是负数,11位为指数为,52位为尾数位。

知道这些现在还不能准确的表述一个数,得有一个约定,约定尾数的形式,不然同一个数字就有多种表示,这个过程称为规格化,形象化的描述就是一个数需要表示成这样的格式: 1.M* 2E,这样的数叫做规格化数。

另外,IEEE还规定了指数的存储形式,64位浮点数指数部分以偏正值形式表示,偏正值为实际的指数大小与1023的和。

到此,就可以将文章开头的两个数用IEEE754的定义表示出来。

对2222222做规格化, 符号位为0,表示是一个正数,2222222对应的二进制是1000011110100010001110,有22位,满足规格化需小数点左移21为,指数就为21 +1023 = 1044,尾数为1.000011110100010001110

最终2222222 的64位二进制浮点数就表示为

0 10000010100 0000111101000100011100000000000000000000000000000000

269567.53的64位浮点数就表示为

0 10000010001 0000011100111111111000011110101110000101000111101100

Java中有对应的函数可以将根据IEEE 754规定,返回指定浮点值的表示形式

Long.toBinaryString(Double.doubleToRawLongBits(2222222))

接下来就是浮点数加减法运算步骤了,如下

  1. 对阶,使两个数的小数点位置对齐

  2. 尾数求和,将运算后的两个尾数求和

  3. 将求和后的尾数规格化

  4. 舍入,考虑尾数右移时的丢失的数值位

  5. 判断结果是否溢出

  • 对阶的原则是小阶向大阶看齐,阶码较小的数尾数向右移,每移一位,阶码加一,在269567.53 + 2222222中,前者向后者看齐,前者尾数右移三位。对阶后两数分别为

     010000010100 0010000011100111111111000011110101110000101000111101100
    
     010000010100 0000111101000100011100000000000000000000000000000000
    
  • 尾数相加

     0100000101000010000011100111111111000011110101110000101000111101100
    
     0100000101000000111101000100011100000000000000000000000000000000
    
     0100000101000011000000101100011011000011110101110000101000111111100
    
  • 规格化

满足规格化

  • 舍入

      0100000101000011000000101100011011000011110101110000101000111111100
    

浮点数舍入的方式有多种,但是不可避免的,都可能会发生数位的丢失,这个就是直接导致浮点数之间运算后出现精度问题的直接原因。

java和javascript中最终舍入后的表示形式为

0100000101000011000000101100011011000011110101110000101000111110

结论

由于二进制表示本身的缺陷,我们不得不面对一个充满舍入误差的规范,进而导致计算结果超乎预期。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容