聊一聊0.1+0.2=0.30000000000000004这件事

本文专为对于0.1+0.2=0.30000000000000004这一结果很懵逼的人打造。

人们都知道在十进制下0.1+0.2的结果等于0.3,这个是答案毋庸置疑的。但是如果有一天你在某个编程语言环境下输入0.1+0.2,但计算机给出的答案是0.30000000000000004,相信不少人一开始一定会大吃一惊,难道计算机连这么简单的加法运算都能算错吗?甚至有人专门建了一个叫做0.30000000000000004.com的网站来记录各种编程语言对于0.1+0.2的计算结果。

0.1和0.2的二进制表示

我们先来看一下十进制数0.1和0.2如何用二进制表示。0.1和0.2都是小数,对于小数需要采用乘2法来计算它们的二进制表示,也就从一个给定的小数开始,不断乘以2,对于每一轮计算的结果,如果个位是0,就提取0,然后继续计算,如果个位是1,就提取1,然后将个位置0并继续计算,如下:

1
2
3
4
5
6
7
8
9
10
11
12
0.1 * 2 = 0.2 -> 0
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
...

0.1的二进制表示中出现了无限循环的情况,也就是(0.1)10 = (0.00110011001100…)2

再来看0.2的二进制表示:

1
2
3
4
5
6
7
8
9
10
11
12
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
...

0.2的二进制表示中也有无限循环的情况,也就是(0.2)10 = (0.01100110011001…)2

通过上述转换,我们可以发现计算机无法用二进制精确地表示某些十进制小数

浮点数运算标准IEEE-754

要理解浮点数运算,就必须提到IEEE-754浮点数运算标准。该标准被运用在大部分CPU和浮点运算器上,可以说是计算机浮点数运算的事实标准,它主要由加州大学伯克利分校的William Morton Kahan教授研究和制定。本文并不打算完整讲述IEEE-754,如有需要可以移步wikipedia-IEEE_754,下面将只介绍IEEE-754知识的一个子集。

IEEE-754规定一个浮点数由三个域组成,如下图:

  1. sign 符号位
  2. exponent 指数偏移值
  3. fraction 分数值

计算一个IEEE-754浮点数的真值公式如下:

1
value = sign * exponent * fraction

IEEE-754浮点数的三个域(图片来源于Wikipedia)

IEEE-754规定单精度浮点数(32 bits)和双精度浮点数(64 bits)的结构如下:

IEEE-754单精度和双精度浮点数格式

32位IEEE-754浮点数的真值计算公式:

value = (-1)S 2E-127 (1.M)

64位IEEE-754浮点数的真值计算公式:

value = (-1)S 2E-1023 (1.M)

上述两个公式中,对于32位浮点数,E = e + 127,对于64位浮点数,E = e + 1023。

可以用一种简单的方法将一个浮点数的二进制形式转换成IEEE-754格式,步骤如下:

  1. 将浮点数的二进制表示为科学计数法形式。
  2. 将该浮点数的科学计数法表示的符号位提取出来,正数为0,负数为1,作为S。
  3. 将该浮点数的科学计数法表示的2的次方提取出来,作为e。
  4. 将该浮点数的科学计数法表示的尾数提取出来,作为M。
  5. 计算(-1)S 2E-127 (1.M)的值,即为该IEEE-754格式浮点数的真值。

按照上面的步骤我们以双精度浮点数来计算0.1+0.2。将下面两个序列以二进制相加:

(0.1)10 = (0.00110011001100…)2
(0.2)10 = (0.01100110011001…)2

我们得到:

(0.3)10 = (0.1001100110011001…)2

同样可以发现,计算机也无法精确地表示十进制数的0.3。

将0.3的二进制形式转换为科学计数法形式:

(0.3)10 = (1.001100110011001…)2 * 2-1

于是得到 S = 0, E = -1 + 1023 = 1022,M = (0.001100110011001…)2

所以在IEEE-754的规定下,由上述S,E和M所表示的64位双精度浮点数的真值就是:

value = (-1)0 21022-1023 (1.001100110011001…)2

64位双精度浮点数的M长度是52位,因此(0.001100110011001…)2实际上被存储为(0001100110011001100110011001100110011001100110011010)2,请注意最后两位,按照之前的规律应该是01(很容易发现0.3的二进制形式中包含循环模式1001),怎么变成10了呢?实际上这是舍入操作导致的。IEEE-754规定了四种舍入方式:

  1. 舍入到最接近,这是默认的舍入方式,会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以0结尾的)
  2. 朝+∞方向舍入
  3. 朝-∞方向舍入
  4. 朝0方向舍入

如过要对10011001舍入左起第3位(从0开始)之后的值,则有两种选择:

  1. 1001 (10011001-00001001)
  2. 1010 (10011001+00000111)

由于舍入到1010(+7)比舍入到1001(-9)更接近10011001,因此上面的M的最后两位就是10而不是01了。

验证

说了这么多,还需要验证我们的理论。计算验证很简单,可以写一段Python代码来验证这个结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
def bin2dec(b):
bit_seq = [int(bit) for bit in b if bit != '.']
value = 0
for i, bit in enumerate(bit_seq):
value += 2**(-i-1) * bit
return value

S = 0
e = -1
E = e + 1023
M = '.001100110011001100110011001100110011001100110011010'

print((-1)**0 * 2**(1022-1023) * (bin2dec('1' + M))) # 0.30000000000000004