浮点数加法运算带来的困惑

缘起:

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

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. 判断结果是否溢出


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

010000010100 0010000011100111111111000011110101110000101000111101100

010000010100 0000111101000100011100000000000000000000000000000000

2. 尾数相加

0100000101000010000011100111111111000011110101110000101000111101100

0100000101000000111101000100011100000000000000000000000000000000

0100000101000011000000101100011011000011110101110000101000111111100

3. 规格化

满足规格化

4. 舍入

0100000101000011000000101100011011000011110101110000101000111111100

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

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

0100000101000011000000101100011011000011110101110000101000111110



结论

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



你可能感兴趣的:(浮点数加法运算带来的困惑)