注意
本文最后更新于 2023-08-31,文中内容可能已过时。
为什么会有无限小数
当我们使用十进制时,我们可以准确表示 $\frac{1}{2}$、$\frac{1}{4}$、$\frac{1}{5}$、$\frac{1}{8}$、$\frac{1}{10}$ 这些小数(0.5、0.25、0.2、0.125、0.1),但是却无法准确表示 $\frac{1}{3}$、$\frac{1}{6}$、$\frac{1}{7}$、$\frac{1}{9}$ 这些小数。其原因在于 2、5 是 10 的质因数,如果分母是质因数的整数倍,就可以用小数准确表示,否则无法被准确表示,比如 $\frac{1}{3} = 0.33333333…$ 。
为什么二进制中,0.1 + 0.2 != 0.3?
与十进制同理,在二进制中,唯一的质因数是 2,因此我们只能清楚地表达 $\frac{1}{2}$、$\frac{1}{4}$、$\frac{1}{8}$ (2的倍数),而 $\frac{1}{5}$、$\frac{1}{10}$ 则为无限小数。因此,0.1 和 0.2 虽然在十进制中是干净的小数,但在计算机使用的二进制中却是无限小数。
准备知识:小数的进制转换
想了解计算机底层如何存储浮点数,需要先了解小数是如何从十进制转为二进制的,毕竟计算机只能存储二进制数据。
下图展示了十进制数 8.625 转换为二进制数 1000.101 的过程,整个过程包含两部分:整数部分的转换和小数部分的转换

计算机存储浮点数的精度问题
上文我们提到了二进制无法准确表达 0.1,这里我们通过数学计算实际感受一下计算机存储浮点数的精度问题:

IEEE 754 浮点数存储标准
在数学上我们总有办法通过额外的符号表示更复杂的数字,但是从工程的角度来看,表示无限精度的数字是不经济的,我们期望通过更小和更快的系统表示范围更大和精度更高的实数。
浮点数系统是在工程上面做的权衡,IEEE 754 就是在 1985 年建立的浮点数计算标准,它定义了浮点数的算术格式、交换格式、舍入规则、操作和异常处理。在今天,几乎所有的编程语言都按照 IEEEE 754 标准来实现浮点数,它的存储格式如下:

解读:
- sign:表示浮点数的正负(0:正数,1:负数)。
- exponent:表示指数位,exponent = 移动位数 + 127。
- mantissa:表示小数位,代表小数点后面的数值。
你可能会有点懵,没关系,我们通过例子来看看计算机如何存储十进制小数 10.625:

解读:存储过程分为三步,
- 十进制转换为二进制。
- 移位,类似科学计数法。
- 用 IEEE 754 标准存储浮点数。
IEEE 754 二进制浮点数转换为十进制小数
IEEE 754 定义了如下公式,将二进制浮点数转换为十进制小数:

接下来我们演示 IEEE 754 二进制浮点数转换为十进制小数的过程:

用 Go 代码一探究竟
下面的 Go 语言代码展示了二进制小数与十进制小数的转换过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
func TestName(t *testing.T) {
var number float32 = 0.085
// 转换为 32 位二进制小数
bits := math.Float32bits(number)
binary := fmt.Sprintf("%.32b", bits)
fmt.Println("转换为 32 位二进制小数:")
fmt.Printf("%b \n", bits)
// 按照 IEEE 754 格式打印
fmt.Println("按照 IEEE 754 格式打印:")
fmt.Printf("Bit Pattern: %s | %s %s | %s %s %s %s %s %s \n", binary[0:1],
binary[1:5], binary[5:9],
binary[9:12], binary[12:16], binary[16:20],
binary[20:24], binary[24:28], binary[28:32])
bias := 127
sign := bits & (1 << 31)
exponentRaw := int(bits >> 23)
fmt.Printf("指数位:%b \n", exponentRaw)
exponent := exponentRaw - bias
fmt.Printf("移位数:%d \n", exponent)
// 计算小数
var mantissa float64
for index, bit := range binary[9:32] {
if bit == 49 {
position := index + 1
bitValue := math.Pow(2, float64(position))
fractional := 1 / bitValue
mantissa = mantissa + fractional
}
}
// IEEE 754 公式
value := (1 + mantissa) * math.Pow(2, float64(exponent))
fmt.Printf("Sign: %d Exponent: %d(%d) Mantissa: %f Value: %f \n",
sign, // 标志位:0
exponentRaw,
exponent, // 指数:-4
mantissa, // 小数位
value) // 小数值
}
|
输出:
1
2
3
4
5
6
7
8
9
|
=== RUN TestName
转换为 32 位二进制小数:
111101101011100001010001111011
按照 IEEE 754 格式打印:
Bit Pattern: 0 | 0111 1011 | 010 1110 0001 0100 0111 1011
指数位:1111011
移位数:-4
Sign: 0 Exponent: 123(-4) Mantissa: 0.360000 Value: 0.085000
--- PASS: TestName (0.00s)
|
原文链接:底层揭秘:计算机如何存储浮点数
参考:
- https://0.30000000000000004.com/
- https://zhuanlan.zhihu.com/p/646206405
- https://zhuanlan.zhihu.com/p/102519285
- http://c.biancheng.net/view/314.html