返回首页

浮点数 / IEEE 754 / 计算机基础

1+2 为什么不等于 3?解析计算机中的浮点数

本文介绍了计算机中对浮点数的编码方式 IEEE 754,并对其出现的成因以及带来的一些问题进行分析。

0.1 与 0.2 两张纸片流入中心天平,右侧涌出带珊瑚色多余零位的 0.30000000000000004 纸条

数据的表示、精度和范围

在由香农创造的信息论中可以知道:如果编码的长度是固定的,那么它能够表示的不同信息的数量也是有限的。虽然可以通过各种编码方式(例如哈夫曼编码)增加固定长度编码所能表达的信息数量,但依旧是有限的。

如果我们给定一个字节用来表示数字,假设采用二进制:对于正整数来说,能表达的范围是 0, 1, 2, 3, ..., 127,这个范围并不大,但精度是 1,只能表达个位数。比如可以用 00000001 表示 1、00000011 表示 3,但不能表示 1.1、112.5、99.9 这样的小数。

如果对于同样一个字节,我们想要提高精度,可以让 00000001 表示 0.1、00000011 表示 0.3。这样精度变成 0.1,数字表示的范围就是 0, 0.1, 0.2, ..., 12.7

如果既想增加范围、又想增加精度,唯一的办法就是增加编码长度

32 位二进制数字可以表示大约 43 亿个不同的数字。如果精度是 0.0001,那么大约可以表示 430 万个不同的数字。这已经很大了,但不足以描述所有业务场景——例如地球的质量大约是 5.965 × 10²⁴ 千克,氢原子的质量大约是 1.674 × 10⁻²⁷ 千克。想要通过增加编码长度同时表示这两种数量级,需要 170 位二进制。这不是一个有效的办法。

IEEE 754

为了解决上述问题,想要在编码长度固定的条件下,兼顾数据的显示范围和精度,IEEE 提出了浮点数规范 IEEE 754,包含三个部分:正负号、尾数、指数

我们学 C 语言时都学过单精度和双精度浮点数。单精度是 4 字节 32 位,双精度是 8 字节 64 位。以双精度为例:

位数含义
0正负号(0 是正,1 是负)
1–11指数部分
12–64尾数部分

浮点数的表达式:

V = (-1)^S × M × R^E

其中 S 是正负、M 是尾数、R 是基数(2)、E 是指数。

尾数部分

首先把一个十进制的数转换成二进制。

十进制转二进制

十进制整数转二进制采用”除 2 取余,逆序排列”。例如十进制数 11:

11 / 2 = 5 ... 余 1
 5 / 2 = 2 ... 余 1
 2 / 2 = 1 ... 余 0
 1 / 2 = 0 ... 余 1

所以 (11)₁₀ 的二进制是 (1011)₂。

十进制小数转二进制小数,采用”乘 2 取整,顺序排列”。例如 0.625:

0.625 × 2 = 1.25  ... 取整数部分 1
0.25  × 2 = 0.5   ... 取整数部分 0
0.5   × 2 = 1     ... 取整数部分 1

顺序排列,所以 (0.625)₁₀ = (0.101)₂。

因此,11.625 的纯二进制表示是 (1011.101)₂

规范化浮点数——尾数的标准化

浮点数之所以是”浮点”的,是因为小数点位置不一定固定。为了固定小数点,IEEE 规定:在二进制数中,通过移位,将小数点前面的值固定为 1。IEEE 754 称这种形式的浮点数为规范化浮点数(normal number)。

比如十进制数 0.15625,转为二进制是 0.00101。为了让第 1 位为 1,执行逻辑右移 3 位,尾数部分成为 1.01。因为右移了 3 位,所以指数部分是 -3。因为规定第 1 位永远为 1,可以省略不存——这样尾数部分多了 1 位,只需存 0100(要记住:这是小数点后的数字,实际是 0.01,转为十进制是 0.25——没算未存的小数点前面的 1)。

尾数精度的限制

对于浮点数来说,尾数最长是 53 位,能表示的最大值是 2⁵³ = 9,007,199,254,740,992。所以能表示的小数极限是 0.9007199254740992,也就是说精度最大就是 15 位

指数部分

因为指数可以是正数也可以是负数,指数部分采用了 The Biased exponent(有偏指数)。IEEE 754 规定:2^(e-1) - 1 的值表示 0,其中 e 是指数部分的位数,小于这个值表示负数,大于这个值表示正数。

因此对于单精度浮点数,2^(8-1) - 1 = 127 是 0;双精度浮点数,2^(11-1) - 1 = 1023 是 0。

结合尾数和指数的规定,IEEE 754 单精度浮点数中,十进制 0.15625 对应的二进制内存表示是:

0 01111100 01000000000000000000000

0.1 + 0.2 = 0.30000000000000004?

假如 0.1 采用二进制来表示,会得到 0.0001100110011... 无限循环。

因为 0.1 无法用有限二进制精确表示,只能取一个”最接近 0.1 的浮点数”。0.2 同理。两个近似值相加,自然得到一个不是精确 0.3 的近似值——0.30000000000000004

这就是几乎所有语言里 0.1 + 0.2 !== 0.3 的根源:不是语言的 bug,是浮点数标准的固有精度损失。

特殊值

infinity(无穷)

当指数位全是 1、尾数位全是 0 时,这样的浮点数表示无穷。根据符号位,有正无穷和负无穷(+infinity-infinity)。

为什么需要无穷? 因为计算机资源有限,没法表示所有的数。当一个数超过了浮点数的表示范围时,就可以用 infinity 表示。数学中也有无穷的概念。

在 Go 语言中,通过 math 包的 Inf(sign int) float64 函数可以获取到正负无穷。在 Java 中,通过 Float.POSITIVE_INFINITYFloat.NEGATIVE_INFINITY 等常量。

具体位模式的常量表示:

  • 正无穷:0x7FF0000000000000
  • 负无穷:0xFFF0000000000000

NaN

NaN 是 not-a-number 的缩写。为什么需要它?例如,当对 -1 进行开根号时,浮点数不知道如何计算,就会返回 NaN。

NaN 的具体内存表示是:指数位全是 1,尾数位不全是 0

和 infinity 类似,Go 和 Java 都定义了相应的函数或常量。

参考文章