一个让无数程序员抓狂的问题
打开浏览器的控制台 输入 0.1 + 0.2 看看结果是什么
0.30000000000000004
不是 0.3 差了那么一点点 这不是 bug 这是所有现代计算机都有的特征 原因藏在计算机表示数字的方式里
整数完美 小数不完美
计算机处理整数很简单 用二进制就能精确表示任何整数 1 是 1 2 是 10 3 是 11 255 是 11111111 每一位的 1 叠起来结果刚刚好
但小数不是这样
十进制里 0.1 就是 1/10 但在二进制里 分母必须是 2 的幂 1/2 1/4 1/8 1/16……1/10 不是 2 的幂 所以二进制无法精确表示 0.1
就像你用十进制写 1/3 只能写成 0.333333…… 永远循环下去一样
在二进制里 0.1 也是一个无限循环小数
0.0001100110011001100110011……
无穷无尽
IEEE 754 的标准答案
计算机不能存无限的数字 1985 年科学家们制定了一个标准 IEEE 754 来规定计算机如何用有限的空间存无限的小数
思路很简单 把一个数字写成科学记数法 然后只保留有效数字
任何一个浮点数可以写成
± 1.xxxxx × 2^y
1.xxxxx 是尾数(mantissa) y 是指数(exponent)
在 64 位浮点数(double 大多数编程语言的默认类型)里 这些信息被塞进 64 个 bit
- 1 位:符号位(正或负)
- 11 位:指数
- 52 位:尾数
问题就在这里 尾数只有 52 位 0.1 的二进制表示是无限循环的 52 位之后的部分全部被截断了
这就是误差的来源 不是 bug 是物理限制
不是计算机笨 是它太诚实
你可能会问 为什么计算器能算对 0.1 + 0.2
因为计算器根本不用浮点数 它要么用十进制编码 要么用高精度整数模拟 代价是慢
计算机选择浮点数的原因很简单 快 现代 CPU 里有专门的浮点运算单元(FPU)一次加法只要几个时钟周期 如果用十进制模拟 速度会慢几十倍甚至上百倍
所以这不是计算机的错 是它在速度与精度之间做出的理性选择
误差会累积
单次误差只有 4e-17 微不足道 但在大量运算中 误差会悄悄累积
想象一个银行系统 每天 100 万笔交易 每笔误差 1e-15 一年下来误差可能达到 0.36 元 十年就是 3.6 元 不算多 但要是交易量再大一个量级呢
这就是为什么金融系统不用 float 和 double 而是用 decimal 或者把元换算成分再用整数
炸弹也有利用浮点数误差引爆的例子 1991 年海湾战争中 爱国者导弹因为浮点数计时误差累积了 0.34 秒 没能拦截到飞毛腿导弹 28 名美军士兵因此丧生 一个只有 4e-17 的微小误差在累积了 100 小时后变成了致命的 0.34 秒
几个实用的结论
- 永远不要用 == 直接比较浮点数 要用容差比较 比如判断两个浮点数之差的绝对值是否小于 1e-9
- 金融计算永远用 decimal 银行不会用 float 算你的利息
- 大数加小数可能等于大数 如果 100000000.0 加上 0.0000001 由于精度限制 结果可能还是 100000000.0 这叫灾难性抵消
浮点数不是 bug 是设计
IEEE 754 的设计是计算机科学历史上最优雅的妥协之一 它用 64 位空间覆盖了从 5e-324 到 1.8e308 的惊人范围 精度虽然有损失 但对大部分科学计算来说已经足够
下一次你看到 0.1 + 0.2 = 0.30000000000000004 别再觉得计算机笨了 它只是在诚实地告诉你
你的数字 超出了我能精确表示的范围