在上方计算器中输入 0.1 + 0.2 并按等号。显示的答案是 0.3 — 因为该工具在显示前将输出结果舍入到小数点后十位。但底层的原始运算结果是 0.30000000000000004。这个微小的差异并非凭空出现;它是一个更深层现象的可见表象:每次浮点运算都会留下舍入残留,当你将足够多的运算串联起来时,这些残留会累积成一个重要的误差。
为什么计算机无法精确存储0.1
计算机以 二进制 形式存储数字——由1和0组成的系统。正如分数 1/3 无法用十进制精确表示(它会无限变成 0.3333…),数字 0.1 也无法用二进制精确表示。计算机在JavaScript使用的64位双精度格式中能达到的最接近值大约是 0.1000000000000000055511151231257827021181583404541015625。
这种表示方式由IEEE 754标准规范,该标准定义了所有现代处理器如何处理十进制数字。标准保证每次算术运算返回最接近的可表示结果——但“最接近”并不等于“精确”。这个差距,无论多小,定义上就是舍入误差。
误差如何在链式运算中增长
单个舍入误差 5 × 10⁻¹⁷ 几乎无法察觉。关键是当你用这个略有偏差的结果作为下一步运算的输入,接着又用该结果进行下一步运算,如此反复。正如维基百科关于舍入误差所描述:“当涉及任何舍入误差的输入进行一系列计算时,误差可能会累积,有时甚至主导整个计算。”
误差增长由两种机制驱动:
- 加法漂移——在长时间求和(累加许多小值)中,每次加法可能都向同一方向舍入。个别误差不会相互抵消,而是叠加。加一千个每个带有
10⁻¹⁵误差的数字,最终误差可达约10⁻¹²——绝对值仍然很小,但比每步误差大一百万倍。 - 减法抵消——减去两个几乎相等的数字会显著放大相对误差。如果两个值在第15位小数上不精确,且前14位数字相互抵消,误差就会主导剩余部分。计算器在三角函数和其他近似抵消的情况下使用此机制,这也是它们的三角函数实现特别谨慎的原因。
重复舍入的具体示例
每步舍入而非最后一步舍入会产生比单次舍入更大的复合误差。将 9.945309 舍入到两位小数得到 9.95(误差:0.0047)。再将其舍入到一位小数得到 10.0(总误差:0.055)。而一次性将 9.945309 舍入到一位小数得到 9.9,总误差仅为 0.045。两步舍入路径引入了额外误差,因为第一次舍入将值推过了下一个阈值。
这就是为什么显示中间结果的计算——例如,将一个舍入值从一个计算复制到另一个计算——可能产生与保持全部精度在一个连续表达式中不同的最终答案。
计算器如何管理舍入误差
上述计算器在评估每个表达式后应用最终舍入步骤:原始浮点结果在显示前舍入到小数点后十位。内部代码使用 Math.round(result * 1e10) / 1e10,它能清除大多数日常计算中的浮点残留。这就是为什么 0.1 + 0.2 显示为 0.3,而非原始的 0.30000000000000004。
这是一个务实的选择:它消除了大多数结果末尾的干扰噪音。也意味着计算器不会显示小于 10⁻¹⁰ 的误差,无论这些误差如何产生。对于绝大多数实际计算——作业、预算、工程估算——十位有效小数的精度已远超需求,因此这种舍入是一个优点,而非妥协。
何时累计误差才真正重要
日常使用中,累计舍入误差很少成为问题。它在以下情况中才相关:
- 非常长的求和——手动逐步累加数百个财务数字或测量读数,而非一次性表达式计算。
- 迭代公式——输出作为下一步输入反馈多次的计算,如数百期的复利计算。
- 近似抵消——减去两个几乎相等的量,暴露通常隐藏在右侧的舍入残留。
- 手动中间舍入——将舍入后的中间结果复制到下一步,而非保持在同一表达式中。
实用的缓解方法很简单:尽可能在一个表达式中输入完整公式,避免手动舍入中间值,如果任何输入值本身是近似的,则将多位小数结果视为近似而非精确答案。
自我检测:在上方计算器中输入0.1 + 0.2— 你会看到0.3。显示结果被清理过,但底层运算并非精确。对于长链步骤,尽量保持完整表达式在一行内:计算器会内部处理累计舍入,而不是你通过复制舍入中间值引入额外漂移。