为什么 0.6+0.3 ≠ 0.9
小麦2024年06月13日1132 字
今天我想告诉你为什么有的时候,0.6 加上 0.3 并不等于 0.9。
0.6 + 0.3 的答案是多少,我相信就连幼儿园的小朋友都会脱口而出是 0.9。
但是前端程序员朋友会告诉你,不是哦,不是 0.9。
而是 0.8999999999999999。啊?你可能会大吃一惊,这是什么奇葩逻辑?实际上类似的例子还有很多,比如:
0.1 + 0.2 不是 0.3。
0.7 - 0.2 也不是 0.5。
1.3 * 3 也不是 3.9。
但是 0.5 + 0.1 又的的确确是 0.6。难道说这就是传说中的 bug?实际上,这个问题不仅出现在 JS 程序中,
也会出现在其他高级语言编写的应用里,只是 JS 程序通常直接运行在你日常交互的各种 APP 界面中,被发现的概率相比其他语言要高。那么问题出在哪里呢,要解开这里面的秘密,我们需要先了解浮点数在计算机中的存储方式。
我们生活中所用的十进制小数,比如 0.3,是由整数和小数两部分组成,但是计算机使用的是二进制表示,因此引入浮点数的概念。浮点数由一个叫做 IEEE 754 的标准定义,广泛应用于计算机系统、编程语言和硬件中。该标准定义了浮点数的表示、舍入行为、运算精度和异常处理等方面的规范。
JS 语言使用双精度浮点数(简称 FP64 或者 Float64)来表示所有数字,这是它在计算机中的存储结构,
这里的 64 是指一共有 64 个比特位,每一位可以存一个 0 或 1。最左边 sign 是符号位,0 表示正数,1 表示负数;绿色部分一共 11 bit,表示指数;红色部分一共 52 bit,表示小数。那么任意一个浮点数可以用这样一个公式表示。
需要注意的是,这里的 1.b51b50…b0 仅仅表示位置关系而不是相乘,下标 2 也表示括号里是一个二进制数。
我们以十进制数 2.25 为例,它对应的二进制数是 10.01,整数部分 2 对应 10,小数部分 0.25 对应 0.01。
我们知道,在十进制科学计数法中,12345 可以表示为 1.2345 x 104,
同理 10.012 用二进制科学计数法就可以表示为 1.001 x 21,这里底数 10 和 2 可以看作几进制。
对应到上面的公式中,可以得到 sign = 0, b51 = 0, b50 = 0, b49 = 1, b48…b0 = 0, e = 1024 = 100000000002 二进制数,写在一起就是下面这一串数字:
上面这个例子中,2.25 的小数部分可以用有限的二进制小数表示,但并非所有的小数都是如此,比如:
23.3 的二进制数为 10111.0100 1100 1100 无限循环 1100,
用二进制科学计数法改写为:1.01110100 1100 1100 … 1100 循环 x 24
参考公式得到各参数,显然小数部分超过了 52 位,超出部分的首位为1,因此向上舍入,最终为:
若反向还原为十进制数,则 23.3 采用双精度浮点数结构存储后,实际上变成了 23.3000000000000007。
大家可以使用这个工具进行转换和查看直观的二进制表示:https://www.binaryconvert.com/convert_double.html
让我们回到一开始的那个例子,0.6 + 0.3 在计算中是如何计算的,这里 0.6 实际是 0.599999999999999977795539507497,
而 0.3 实际上是 0.299999999999999988897769753748,
因此结果为 0.899999999999999966693309261245,
由于双精度浮点结构无法存储这么长的小数,将越界部分舍弃后即为:0.8999999999999999,也就是开头出现的诡异结果。
总结一下,之所以 0.6+0.3 ≠ 0.9,是因为计算机在采用双精度浮点数结构,存储二进制小数的过程中,可能会出现无限循环,由于存储空间有限,不得不做出取舍,从而导致实际存储的数字与原数字并不一定精确相等,出现精度问题。