0.1 加 0.2 为什么不等于 0.3?

861 阅读5分钟

2021-02-10更新: 这篇文章我并非完全理解,有一些疏漏之处。我重写整理了思路,写了一篇,对浮点数机制进行了更完备的说明,传送门:0.1 加 0.2 不等于 0.3 ?从计算机角度深挖 JavaScript 浮点数存储机制


写在前面

之前面试遇到过一个问题:为什么 JavaScript 里面 0.1+0.2 !== 0.3。当时我就回答到了浮点数精度有误差,显然面试官不满意,这不谁都知道吗?

小红书上也强调过不要进行 0.1+0.2 === 0.3 的判断,但是仅仅提到如下理由:

关于浮点数值计算会产生舍入误差的问题,有一点需要明确:这是使用基于 IEEE754 数值的浮点计算的通病,ECMAScript 并非独此一家;其他使用相同数值格式的语言也存在这个问题。

上面仅仅提到 ECMAScript 使用了基于 IEEE754 标准的浮点数存储机制导致了误差,但是深入 IEEE754 标准,甚至深入计算机层面,又是为什么?这篇文章会从计算机组成原理出发,来探讨这个问题。

什么是浮点数?

计算机中数的表示

计算机中数的表示有定点数和浮点数。

  • 定点数:小数点位置确定。假设16位存储,小数点在符号位后面成为小数定点机(这类机器只能表示小数),小数点在末尾成为整数定点机(这类机器只能表示整数)。
  • 浮点数表示类似于十进制的科学计数法表示。由阶数(阶码,阶符),尾数(尾码,尾符)构成。尾数的位数决定了浮点数的精度,阶数的位数决定了浮点数的范围。

浮点数的规格化

  1. 对于小数来说,转换为二进制是乘 2 取整操作,例如将 0.1 转换成二进制

    运算过程取整小数部分
    0.1 * 2 = 0.200.2
    0.2 * 2 = 0.400.4
    0.4 * 2 = 0.800.8
    0.8 * 4 = 1.610.6
    0.6 * 2 = 1.210.2
    .........
  2. 基于 IEEE74 标准(见下面部分)的双精度浮点数(尾数最多 52 位),我们可以得到如下结果:

0.1=0.00011001100110011001100110011001100110011001100110011001520.1 = 0.0001\quad1001100110011001100110011001100110011001100110011001(52)
  1. 规格化处理
  • 规格化定义(r 表示阶基值,这里为 2,S 表示尾数)

    0.1=0.00011001100110011001100110011001100110011001100110011001520.1 = 0.0001\quad1001100110011001100110011001100110011001100110011001(52)
  • 对于负数形式的补码,规格化的定义不适用(下面负数补码表示-1,不满足规格化定义)

    S>0规格化形式S<0规格化形式
    真值0.1XX...X真值-0.1XX...X
    原码0.1XX...X原码1.1XX...X
    补码0.1XX...X补码1.0XX...X
    反码0.1XX...X反码1.0XX...X
  • 因此通常来说

    对于原码,不论整数,负数,第一数位为 1 即为规格化

    对于补码,符号位和第一数位不同为规格化

    在计算机中我们通常使用异或电路,当符号位和第一数位不同时,表示规格化完成

IEEE754 标准

  • 数字存储格式:S(数符)+ 阶码(含阶符)+ 尾数
  • 尾数为规格化表示
  • 非“0”的有效位最高位为“1”(隐含)
符号位 S阶码尾数总位数
短实数(单精度)182332
长实数(双精度)1115264
临时实数1156480

0.1 和 0.2 在计算机中是如何存储的

上面我们求出了 0.1 的二进制表示,同理可以求出 0.2 的(52 表示尾数为 52 位)

0.1=0.00011001100110011001100110011001100110011001100110011001520.2=0.001100110011001100110011001100110011001100110011001100152\begin{aligned} 0.1 = 0.0001\quad&1001100110011001100110011001100110011001100110011001 (52)\\ 0.2 = 0.001\quad&1001100110011001100110011001100110011001100110011001 (52) \end{aligned}

计算机中浮点数阶码(P)一般使用移码表示,尾数(S)使用补码表示,小数点前一个 1 做隐含处理。求得 0.1 和 0.2 的阶码和尾数如下

S0.1=1.10011001100...11100×12S0.2=1.100110011...0010011×12P0.1=1,00000001004P0.2=1,00000000113\begin{aligned} S(0.1) = 1.10011001100...1 (1100\times12)\\ S(0.2) = 1.100110011...001 (0011\times12)\\ P(0.1) = 1,0000000100(-4)\\ P(0.2) = 1,0000000011(-3) \end{aligned}

将阶数使用移码表示,存入计算机(中括号中为隐含的 1)

0.10:01111111100:10011001100110011001100110011001100110011001100110100.20:01111111101:10011001100110011001100110011001100110011001100110100.1=24×[1].10011001100110011001100110011001100110011001100110100.2=23×[1].1001100110011001100110011001100110011001100110011010\begin{aligned} &0.1 \Rightarrow 0:01111111100:1001100110011001100110011001100110011001100110011010\\ &0.2 \Rightarrow 0:01111111101:1001100110011001100110011001100110011001100110011010\\ \\ &0.1 = 2^{-4} \times [1].1001100110011001100110011001100110011001100110011010\\ &0.2 = 2^{-3} \times [1].1001100110011001100110011001100110011001100110011010 \end{aligned}

0.1+0.2 在计算机中是如何运算的

首先需要対阶,小阶向大阶看齐(小阶的尾数减小,只需右移,损失精度而不会造成错误),这里阶差为 1

注意这里作为小阶的 0.1 右移添补的是隐含的“1”,而不是默认右移添 0

0.1=23×0.1100110011001100110011001100110011001100110011001101(0)0.2=23×1.1001100110011001100110011001100110011001100110011010sum=23×10.0110011001100110011001100110011001100110011001100111\begin{aligned} 0.1 = 2^{-3}\times&0.1100110011001100110011001100110011001100110011001101(0)\\ 0.2 = 2^{-3}\times&1.1001100110011001100110011001100110011001100110011010\\ sum = 2^{-3}\times1&0.0110011001100110011001100110011001100110011001100111\\ \end{aligned}

IEEE754 标准浮点数舍入模型

IEEE754 标准对浮点数进行舍入时,一共定义了四种模型

Round to Nearest - roundTiesToEven (Default): 向最近的数靠近,最近的数需满足最低有效位为 0 或者偶数

Round toward 0: 向 0 靠近

Round toward +∞: 向正无穷靠近

Round toward −∞: 向负无穷靠近

第一种模型解决了 50%的舍入情况,还有一种模型叫做Round to Nearest - tiesAwayFromZero,这种模型就和它的名字一样,远离 0 进行舍入

下面是 5 个舍入模型的例子,前四个模型用于 IEEE754 标准下的浮点数舍入,第一个为默认模型:

Mode / Example Value+11.5+12.5−11.5−12.5
to nearest, ties to even (默认模型)+12.0+12.0−12.0−12.0
toward 0+11.0+12.0−11.0−12.0
toward +∞+12.0+13.0−11.0−12.0
toward −∞+11.0+12.0−12.0−13.0
to nearest, ties away from zero+12.0+13.0−12.0−13.0

这时候我们再来看 sum 规格化后,是如何使用上述标准进行舍入的。

误差的产生

首先,sum 做规格化,并隐含 1 后如下(sum 此时位于 a,b 之间)

a=22×1.0011001100110011001100110011001100110011001100110011(0)sum=22×1.0011001100110011001100110011001100110011001100110011(1)b=22×1.0011001100110011001100110011001100110011001100110100(0)\begin{aligned} a = 2^{-2}\times&1.0011001100110011001100110011001100110011001100110011(0)\\ sum = 2^{-2}\times&1.0011001100110011001100110011001100110011001100110011(1)\\ b=2^{-2}\times&1.0011001100110011001100110011001100110011001100110100(0)\\ \end{aligned}

按照上述第一个舍入模型,a 的最低有效位为 1,b 的最低有效位为 0,sum 将使用 b,然后存入计算机中。最后,我们将存入计算机中的 0.3 和 sum 进行一个比较:

0.1+0.2=22×1.00110011001100110011001100110011001100110011001101000.3=22×1.00110011001100110011001100110011001100110011001100110.1+0.20:01111111101:0011001100110011001100110011001100110011001100110[100]0.30:01111111101:0011001100110011001100110011001100110011001100110[011]\begin{aligned} 0.1+0.2=2^{-2}\times&1.0011001100110011001100110011001100110011001100110100\\ 0.3 = 2^{-2}\times&1.0011001100110011001100110011001100110011001100110011\\ \\ 0.1 + 0.2 \Rightarrow 0:01111111101&:0011001100110011001100110011001100110011001100110[100]\\ 0.3 \Rightarrow 0:01111111101&:0011001100110011001100110011001100110011001100110[011] \end{aligned}

最终结果相差了22×252=2542^{-2}\times2^{-52} = 2^{-54}!!!

如果再将上面两个数转换成我们熟悉的十次方:

0.1+0.2=0.300000000000000044408920985006...0.3=0.299999999999999988897769753748...\begin{aligned} 0.1 + 0.2 = &0.300000000000000044408920985006...\\ 0.3 = &0.299999999999999988897769753748... \end{aligned}

控制台输出:

var ll = 0.300000000000000044408920985006; // 中间有15个0
var lll = 0.299999999999999988897769753748;
console.log(ll, lll); //answer: 0.30000000000000004 0.3

总结

  • 一道涉及 JS 基础数据结构—浮点数的问题,深入探究起来,让自己又复习了一波计算机组成原理的知识。

  • 当时学习计组的时候,书上对 IEEE754 标准说得也很少,甚至不知道 IEEE754 的舍入标准。以至于之前的笔记我都认为 sum 的舍入使用的是学过的 0 舍 1 入法。直到写下这篇文章,揭开 IEEE 754 的面纱,才发现没那么简单。

参考

  1. [Is floating point math broken? -- stack overflow(answer by Wai Ha Lee)](stackoverflow.com/a/28679423)

  2. [Rounding floating-point numbers -- Wikipedia](en.wikipedia.org/wiki/IEEE_7…

  3. [IEEE 754: Rounding Rules -- Wikipedia](en.wikipedia.org/wiki/IEEE_7…