浮点数问题,我在早前的文章聊过这个问题,感兴趣的同学点下《lua 开发防坑指南》。最近项目刚好又遇到这个问题,再细致讲下。这里以lua做说明,其他语言道理也是一样的。
在计算机中,二进制表示数字的核心是整数除 2 取余,小数乘 2 取整。小数转换为二进制时,由于精度限制,许多小数无法被精确表示,而是会变成循环二进制小数。以下是0.1的二进制表示(基于小数部分乘2取整法):
0.1 的二进制表示
整数部分1的二进制是 1。
小数部分0.1的二进制转换过程:
0.1 × 2 = 0.2 → 整数部分0
0.2 × 2 = 0.4 → 整数部分0
0.4 × 2 = 0.8 → 整数部分0
0.8 × 2 = 1.6 → 整数部分1
0.6 × 2 = 1.2 → 整数部分1
0.2 × 2 = 0.4 → 整数部分0(从此开始循环)
0.1的二进制是 0.000110011001100110011...,其中 0011 是循环节。
而 float(32 位)仅能存储 23 位尾数位,double(64 位)仅能存储 52 位尾数位 —— 必须对无限循环的二进制小数做舍入截断,最终存储的是 “近似值” 而非原值:
float 型 0.1 的实际值:≈0.10000000149011612
double 型 0.1 的实际值:≈0.10000000000000000555
所以,计算机表达一个小数时,很多都有精度丢失问题。为什么相同的两个小数可以比较,只是丢失的情况一样,就可以进行比较。
精度丢失问题
首先小数无法被完整表达,再者小数进行运算后,误差可能会进一步放大,导致两个看似一样的小数无法进行比较。
> 0.2 == 0.2 true > 0.6 / 3 == 0.2 false
> a=0 > for i=1, 100 do a=a+0.1 end > a == 10.0 false > a 10.0
为了处理无限循环的二进制小数,IEEE 754 规定了舍入规则(默认是向最近值舍入,若相等则向偶数舍入),超出尾数位的部分会被截断,同时调整最后一位(舍入)。
这种调整会导致存储值与原值存在微小偏差,运算时,中间结果也会按此规则舍入,进一步加大误差。
解决精度丢失问题
1. 用整数替代小数:
这种是比较推荐的,一劳永逸。如人名币金额,以分为单位记录整数,避免浮点运算,展示时再除以100
2. 使用格式化进行校准:
> 0.6 / 3 == 0.2
false
> string.format("%.6f", 0.6 / 3) == string.format("%.6f", 0.2)
true
> tonumber(string.format("%.6f", 0.6 / 3)) == 0.2
true
