浮点数精度丢失问题

浮点数问题,我在早前的文章聊过这个问题,感兴趣的同学点下《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

发表评论

邮箱地址不会被公开。 必填项已用*标注