浮点计算精度损失原因
14 Mar 2017 | CS介绍
3.0 - 2.99 竟然不等于 0.01
我们将一个浮点数减去另一个浮点数:
PHP代码
$a = 3.0;
$b = 2.99;
var_dump($a - $b);
#=> 打印的不是 0.01, 而是 float(0.0099999999999998)
在我们的数学知识体系中, 结果是是0.01, 但是在这里结果会是float(0.0099999999999998)
同样类似的情况会在很多语言里面出现:
Ruby代码
a = 6.6
b = 1.3
p a + b
#=> outputs 7.8999999999999995
我们可以发现浮点运算不是百分百正确的
通常来说, float
或者double
已经能满足一般的小数计算需求, 但是在我们设计所有与金融财务价格等相关的应用的时候, 必然会接触到小数的加减乘除运算, 所以必然会出现误差
为什么使用float
和double
会出现精度损失
大部分的CPU是用IEEE二进制浮点数算术标准(IEEE 754)来计算浮点数的
有常用的, 单精确度浮点数float
, 双精确度浮点数double
采用二进制科学计数法, 分为三个部分, 符号位(sign)
, 指数部分(exponent)
和有效部分(fraction, mantissa)
其中float
占用32位, 符号位, 指数部分, 有效部分各占1位, 8位, 23位
double
占用64位, 符号位, 指数部分, 有效部分各占1位, 11位, 52位
计算机二进制角度计算 6.6 + 1.3 的过程
1.转换整数部分 6
转换为二进制 也就是 110
, 1
转换为 1
2.转换小数部分 0.6
转换为二进制
这里给出快速将十进制小数转换为二进制的方法:将小数乘以2
, 取整数部分作为二进制的值, 然后再将小数乘以2
, 再取整数部分, 以此往复循环
根据上表, 将整数部分这列纵向取值, 可以得到小数的二进制的值
乘2结果 | 小数部分 | 整数部分 | 数位 |
---|---|---|---|
0.6 | 0.6 | 0 | 个位 |
1.2 | 0.2 | 1 | 小数十分位 |
0.4 | 0.4 | 0 | 小数百分位 |
0.8 | 0.8 | 0 | 小数千分位 |
1.6 | 0.6 | 1 | - |
1.2 | 0.2 | 1 | - |
0.4 | 0.4 | 0 | - |
0.8 | 0.8 | 0 | - |
1.6 | 0.6 | 1 | - |
1.2 | 0.2 | 1 | - |
0.4 | 0.4 | 0 | - |
0.8 | 0.8 | 0 | - |
1.6 | 0.6 | 1 | - |
1.2 | 0.2 | 1 | - |
0.6 --> 0.10011001100110011001... // 是个无限循环的小数
6.6 --> 110.10011001100110011001...
3.规约化 我们通过规约化将小数转为规约形式, 类似我们用的科学计数法, 就是保证小数点前面有一个有效数字, 在二进制里面, 就是保证整数位是一个 1
110.10011001100110011001 --> 1.1010011001100110011001 * 2^2 //因为是2的2次方, 规约化后的幂即为指数值, 为2
4.指数偏移值
是指浮点数中指数部分的值, 它的值为规约形式的指数值加上某个固定的值, float
的固定值为127, 计算方法是2^e-1 其中的e为存储指数部分的比特位数, 前面提到的float
为8位, double
为11位
在这里, 因为是 2 的 2 次方, 偏移值就是127+2=129
, 转换为二进制就是10000001
5.拼接
6.6
为正数, 符号位为0
, 指数部分为偏移值的二进制10000001
, 有效部分为规约形式的小数部分, 为什么只取小数部分? 因为整数肯定是1
, 去掉了不会产生误差
我们去取小数的前23
位即10100110011001100110011
, 最后拼接到一起即01000000110100110011001100110011
同理, 我们可以计算出1.3
的浮点数为00111111101001100110011001100110
PS: 可以使用这个工具验证是否转换正确IEEE 754 Converter
0 + 10000001 + 1.10100110011001100110011
0 + 10000001 + (1.)10100110011001100110011 // 只要小数部分
0 + 10000001 + 10100110011001100110011
--> 01000000110100110011001100110011
6.求和 因为1.3
比6.6
小, 我们对1.3
进行移位直到两个数的偏移值相同, 然后与6.6
相加
0 01111111 01001100110011001100110 // 1.3
-> 0 10000000 10100110011001100110011 // 1.3 这里前面补位补了1, 就是步骤5被舍弃的部分
-> 0 10000001 01010011001100110011001 // 1.3
+ 0 10000001 10100110011001100110011 // 6.6
---------------------------------------
0 10000001 11111001100110011001100 // 结果
转为十进制
0 10000001 11111001100110011001100 --> 1.11111001100110011001100 * 2^2
2^2 * (2^0 + 2^-1 + 2^-2 + 2^-3 + 2^-4 + 2^-5 + 2^-8 +...+ 2^-21) = 7.899999618530273
至此完成 6.6 + 1.3 的过程, 加减乘除方法类似, 有兴趣可以自己计算看看
精度损失细节探究
我们不难发现, 上面第 5 步, 数据会在存储的时候, 就已经开始出现误差, 运算的时候, 也就是第 6 步, 也会出现误差 我们可以得出以下结论:
- 浮点数精度有限, 只能展示有限的信息
- 浮点数在每次计算的时候, 都会出现误差, 如果运算次数越多, 误差也会越来越大
浮点数使用注意
- 不要用
==
来判断两个浮点数是否相等 - 可以尝试使用两个浮点数差的绝对值来判断是否相等
if ( abs( a - b ) < 0.0000001 )
出现精度损失的几个通用解决办法
使用decimal
Ruby 的 BigDecimal 库就是来处理这个问题
有很多类似的讨论:
Ruby 由小数的精度问题引出设计问题
直接使用整型变量
也有些也可以直接使用整型, 理论上和decimal差不多
为什么要金额使用分为单位保存成整型在数据库中?
Comments