为什么 0.1 + 0.2 == 0.3 为 false?

Reading time ~59 minutes

这是一个经典的关于浮点数的问题,从相关的概念上很容易理解,有些数不能精确存储而导致了误差。但 0.1 + 0.2 = 0.30000000000000004 的结果究竟是怎么得来的呢,想要知道答案需要了解背后的计算规则。

1. IEEE 754

IEEE 754 是一种广泛使用的标准,用于表示计算机系统中的浮点数。它于 1985 年首次由电气和电子工程师协会 (Institute of Electrical and Electronics Engineers,IEEE) 发布,称为“二进制浮点运算的 IEEE 标准”(IEEE 754-1985)。该标准多年来经历了修订,最常引用的版本是“IEEE 754-2008”。

IEEE 754 标准定义了表示单精度和双精度浮点数的各种格式。这些格式通常用于对二进制表示形式的实数执行算术运算,这对于广泛的科学和工程计算至关重要。

标准中定义的两种最常见的格式是:

  1. 单精度(32位):
    • 1 位用于符号(0 为正,1 为负)
    • 8 位指数
    • 23 位用于有效数(也称为尾数)
  2. 双精度(64位):
    • 1 位用于符号
    • 11 位指数
    • 52 位有效数

指数表示数字的大小,而有效数表示其精度或小数部分。该标准还定义了特殊值,例如正无穷大和负无穷大、NaN(非数字)和次正规数。

IEEE 754 还指定了对浮点数执行算术运算的规则,包括加法、减法、乘法和除法,以及二进制和十进制表示之间的舍入和转换方法。

了解与浮点表示相关的某些限制和问题非常重要,例如由于可用位数有限而导致的舍入误差和精度损失。因此,在关键应用中使用浮点数时需要小心,特别是在需要高精度时。

总之,IEEE 754 是一项至关重要的标准,极大地影响了现代计算机系统中浮点运算的实现方式,允许跨不同平台和编程语言对实数进行一致且可预测的处理。

2. 一个简单的转换例子

在看例子之前,要补充一下指数偏移值和规范化尾数的概念。

  1. 指数偏移值(exponent bias),即浮点数表示法中指数域的编码值,等于指数的实际值加上某个固定偏移值,IEEE 754 标准规定该固定值为2^(e-1) - 1,其中的 e 为存储指数的位数。这样正负指数都可以表示。单精度浮点数的固定偏移值为 127,双精度浮点数的固定偏移值为 1023。

  2. 规范化尾数(normalised mantissa)。尾数是科学记数法中数字或浮点数的一部分,由其有效数字组成。这里我们只有 2 位数字,即 0 和 1。因此,标准化尾数是小数点左侧只有一个 1 的尾数。

下面看一个例子:

85.125

整数部分:85 = 1010101
小数部分:0.125 = 0.001
所以:85.125 = 1010101.001 = 1.010101001 * 2^6 
符号位 = 0 

1. 单精度:
偏移后的指数 = 6 + 127 = 133
133 = 10000101
规范化尾数 = 010101001
填充到 23 位

IEEE 754 单精度表示
= 0 - 100 0010 1 - 010 1010 0100 0000 0000 0000
十六进制表示为:42AA4000

2. 双精度:
偏移后的指数 = 6 + 1023 = 1029
1029 = 10000000101
规范化尾数 = 010101001
填充到 52 位

IEEE 754 双精度表示
= 0 - 100 0000 0101 - 0101 0100 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
十六进制表示为 = 4055480000000000 

3. 0.1 和 0.2 的 IEEE 754 表示

0.1

整数部分:0 = 0
小数部分:
0.1 = 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 ...
所以
0.1 = 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 ...
符号位 = 0 

0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 (1001 ...)
进行舍入处理 = 
1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 * (2^-4)

实际指数 = -4
尾数 = 1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

偏移后的指数 = -4 + 1023 = 1019
1019 = 011 1111 1011
规范化尾数 = 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

IEEE 754 双精度表示
= 0 - 011 1111 1011 - 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
十六进制表示为 = 3FB999999999999A

IEEE 754 定义了几种舍入模式,规定当结果无法精确表示时浮点算术运算应如何处理舍入。这些舍入模式用于将算术运算的结果舍入到最接近的可表示值或处理上溢或下溢等特殊情况。IEEE 754 中定义的舍入模式如下:

  1. 舍入到最接近:默认模式,舍入到最接近,在一样接近的情况下偶数优先(Ties To Even):会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以 0 结尾的)。
  2. 朝 +∞ 方向舍入:会将结果朝正无限大的方向舍入。
  3. 朝 -∞ 方向舍入:会将结果朝负无限大的方向舍入。
  4. 朝 0 方向舍入:会将结果朝 0 的方向舍入。

需要注意的是,大多数实现 IEEE 754 的编程语言和平台的默认舍入模式是“舍入到最接近”。此模式是首选模式,它可以在执行一系列操作时最大限度地减少偏差,因为它以相同的概率向上或向下舍入到最接近的值,有助于保持良好的数值属性。

不同的编程语言或库可能提供更改特定操作的舍入模式或以不同方式处理舍入的方法。但是,必须了解更改舍入模式的潜在影响,因为它会影响浮点计算的整体精度和行为。

同理,0.2 的 IEEE 754 表示处理如下:

0.2

整数部分:0 = 0
小数部分:
0.2 = 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 ...
所以
0.2 = 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 ...
符号位 = 0 

0.2 = 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 001(1 0011 ...)
进行舍入处理 = 
1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 * (2^-3)

实际指数 = -3
尾数 = 1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

偏移后的指数 = -3 + 1023 = 1020
1020 = 011 1111 1100
规范化尾数 = 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

IEEE 754 双精度表示
= 0 - 011 1111 1100 - 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
十六进制表示为 = 3FC999999999999A

4. 0.1 + 0.2 !== 0.3

至此:

0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010

或者

0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125

把两个数相加,需要先转换到指数一致:

0.1 => 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum =  2^-3 * 10.0110011001100110011001100110011001100110011001100111

或者

0.1 => 2^-55 * 3602879701896397  = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794  = 0.200000000000000011102230246251565404236316680908203125
sum =  2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875
sum = 
2^-3 * 10.0110011001100110011001100110011001100110011001100111
规范化表示 =
2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)

接下来要对 sum 进行舍入处理,sum 落在 a 和 b 区间:

a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
  = 2^-2  * 1.0011001100110011001100110011001100110011001100110011

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)

b = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
  = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

请注意,sum 一样接近 a 和 b,a 和 b 仅最后一位不同:...0011 + 1 = ...0100。在这种情况下,最低有效位为零(偶数)的值为 b,因此总和为:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
    = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

而 0.3 的二进制表示

0.3

整数部分:0 = 0
小数部分:
0.3 = 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 ...
      0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 ...
      0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 ...
所以
0.3 = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 ...
符号位 = 0 

0.3 = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 11(00 1100 ...)
进行舍入处理 = 
1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 * (2^-2)

实际指数 = -2
尾数 = 1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011

偏移后的指数 = -2 + 1023 = 1021
1021 = 011 1111 1101
规范化尾数 = 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011

IEEE 754 双精度表示
= 0 - 011 1111 1101 - 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011
十六进制表示为 = 3FD3333333333333

也就是:
0.3 => 2^-2  * 1.0011001100110011001100110011001100110011001100110011
    =  2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875

所以出现 0.1 + 0.2 !== 0.3 的结果,左右两边相差 2^-54。

总结来说就是(方括号内为差异部分):

0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
          => 0.3000000000000000444089209850062616169452667236328125
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]
          => 0.299999999999999988897769753748434595763683319091796875

5. 方便的转换和计算工具

参考链接

批量上报

前端批量上报代码片段 Continue reading

页面打开成功率

Published on November 15, 2023

Service Worker 原理与实践

Published on July 08, 2023