缘起:
公司一小姑娘群发邮件求助,邮件内容是关于浮点数之间运算结果莫名其妙的多了一些小数位,小姑娘想不通所以想问问有人知道原因不?涉及的代码是下面的样子
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))
接下来就是浮点数加减法运算步骤了,如下
对阶,使两个数的小数点位置对齐
尾数求和,将运算后的两个尾数求和
将求和后的尾数规格化
舍入,考虑尾数右移时的丢失的数值位
判断结果是否溢出
-
对阶的原则是小阶向大阶看齐,阶码较小的数尾数向右移,每移一位,阶码加一,在269567.53 + 2222222中,前者向后者看齐,前者尾数右移三位。对阶后两数分别为
010000010100 0010000011100111111111000011110101110000101000111101100 010000010100 0000111101000100011100000000000000000000000000000000
-
尾数相加
0100000101000010000011100111111111000011110101110000101000111101100 0100000101000000111101000100011100000000000000000000000000000000 0100000101000011000000101100011011000011110101110000101000111111100
规格化
满足规格化
-
舍入
0100000101000011000000101100011011000011110101110000101000111111100
浮点数舍入的方式有多种,但是不可避免的,都可能会发生数位的丢失,这个就是直接导致浮点数之间运算后出现精度问题的直接原因。
java和javascript中最终舍入后的表示形式为
0100000101000011000000101100011011000011110101110000101000111110
结论
由于二进制表示本身的缺陷,我们不得不面对一个充满舍入误差的规范,进而导致计算结果超乎预期。