从 50% + 50% = 0.75 说起:我儿子的信仰崩塌了

笔者之前的文章 【开箱】知乎社区 2024 年新年礼盒介绍了知乎每年的台历,其中有这样一页我觉得很有意思,它也再次让我儿子,领教了计算机和人类在完成一个任务上的处理差异。

问题:为什么手机计算器上,50% + 50% = 0.75?

我以前从没留意过这个问题。在三星手机上试了一下,还真是这样:

知乎上这个问题的回复:

因为手机计算器(大部分情况下的默认计算器),都按照 a% + b% = a + ab% 或 a(1+b%)计算。

当你输入 50% + 50% 的时候,手机先会把前面一个 50% 转化成 0.5(因为它的前面没有数了,于是就默认转成小数,a% = a/100),后一个就理解为“加上前一个数的 50%”,于是 50% + 50% = 50% + 50% * 50% = 50% + 25% = 75% = 0.75.

手机计算器的这种处理方式,让我儿子觉得有点不可思议。

让他不可思议的事情还在后头。

我想起了自己多年前,刚接触 JavaScript 时,学习到的一些知识点。

比如 JavaScript 里,0.1 + 0.2 = 0.30000000000000004.

我儿子第一次看到 JavaScript 这个计算结果时,也很吃惊。

十进制小数 0.1 转二进制的计算过程:

  • 0.1*2=0.2……0——整数部分为 0 。整数部分 0 清零后为 0,用 0.2 接着计算。
  • 0.2*2=0.4……0——整数部分为 0 。整数部分 0 清零后为 0,用 0.4 接着计算。
  • 0.4*2=0.8……0——整数部分为 0 。整数部分 0 清零后为 0,用 0.8 接着计算。
  • 0.8*2=1.6……1——整数部分为 1 。整数部分 1 清零后为 0,用 0.6 接着计算。
  • 0.6*2=1.2……1——整数部分为 1 。整数部分 1 清零后为 0,用 0.2 接着计算。
  • 0.2*2=0.4……0——整数部分为 0。 整数部分 0 清零后为 0,用 0.4 接着计算。
  • 0.4*2=0.8……0——整数部分为 0 。整数部分 0 清零后为 0,用 0.8 接着计算。
  • 0.8*2=1.6……1——整数部分为 1。 整数部分 1 清零后为 0,用 0.6 接着计算。
  • 0.6*2=1.2……1——整数部分为 1。 整数部分 1 清零后为 0,用 0.2 接着计算。
  • 0.2*2=0.4……0——整数部分为 0。 整数部分 0 清零后为 0,用 0.4 接着计算。
  • 0.4*2=0.8……0——整数部分为 0。 整数部分 0 清零后为 0,用 0.8 接着计算。
  • 0.8*2=1.6……1——整数部分为 1。 整数部分 1 清零后为 0,用 0.6 接着计算。
    … 无限循环下去

最后十进制小数 0.1 的二进制表示是 0.000110011001100(无限循环下去)

同理,十进制小数 0.2 的二进制表示是 0.00110011001100110011(无限循环下去)

导致 JavaScript 里 0.1 + 0.2 = 0.30000000000000004 问题的根源,在于二进制系统无法精确表示一些十进制小数,例如 0.1 和 0.2.

在 JavaScript 和许多其他编程语言中,浮点数表示采用的是 IEEE 754 标准,这种标准下最高的 1 位是符号位 S,接着的 11 位是指数 E,剩下的 52 位为有效数字 M.

这种规范试图用有限的 64 位,去描述一个在二进制表示形式下会无限循环的十进制数,超出有效数字位之外的循环节,会进行舍入(round)操作,因此会导致精度损失。

当 0.1 和 0.2 这两个浮点数相加时,二者各自进行舍入操作导致的精度损失,在相加时会累积,导致最终的结果略微偏离我们预期的 0.3.

再试试 ABAP:

结果和 JavaScript 一致:

所以 ABAP 里 0.3 / 0.1 = 2.9999999999999996 也就不难理解了。

看到这里,你认为只有小数才会遇到这些麻烦?

再来看看这个例子:9999999999999999 = 10000000000000000

编码时,变量 v1 的值还是硬编码的 9999999999999999.

运行这个 ABAP 报表,调试器里一看,9999999999999999 变成了 10000000000000000:

再看这个泥牛入海的 1:10000000000000000 + 1 = 10000000000000000

无论是在循环体外,还是循环体内,如果每次只给 10000000000000000 累加一个 1,这个 1 就像肉包子打狗一样,有去无回。

上面代码里两个 WRITE 语句,打印的仍然是 10000000000000000 这个数。

但如果是一次性给 10000000000000000 加上 2,这个累加的 2 就生效了:

说到底,这些看似怪异的结果,还是因为 IEEE 754 二进制浮点运算标准造成的。

9999999999999999 被舍入成了离它最近的一个偶数 10000000000000000. 而 10000000000000000 加上 1 之后的结果,因为存储精度损失,被舍入回 10000000000000000,因此无论是直接加1,还是在循环里逐次累加,最后的舍入结果仍然为 10000000000000000.

如果对这些例子感兴趣想深入研究,建议阅读 SAP ABAP 帮助文档:Binary Floating Point Numbers 一节。

本来我儿子觉得计算机内部的计算是绝对"精确",永远不会"出错",远远胜过人类的,但是看了这些例子,他对计算机计算能力的信仰崩塌了,囧。

回到本文开头的例子,50% + 50% = 0.75,知乎上被大多数朋友接受的一种说法:这是一种 working as designed 的行为:

猜你喜欢

转载自blog.csdn.net/i042416/article/details/135479784
50
50A