底层揭秘:计算机如何存储浮点数

警告
本文最后更新于 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 的过程,整个过程包含两部分:整数部分的转换和小数部分的转换

1

计算机存储浮点数的精度问题

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

6

IEEE 754 浮点数存储标准

在数学上我们总有办法通过额外的符号表示更复杂的数字,但是从工程的角度来看,表示无限精度的数字是不经济的,我们期望通过更小和更快的系统表示范围更大和精度更高的实数。

浮点数系统是在工程上面做的权衡,IEEE 754 就是在 1985 年建立的浮点数计算标准,它定义了浮点数的算术格式、交换格式、舍入规则、操作和异常处理。在今天,几乎所有的编程语言都按照 IEEEE 754 标准来实现浮点数,它的存储格式如下:

2

解读:

  • sign:表示浮点数的正负(0:正数,1:负数)。
  • exponent:表示指数位,exponent = 移动位数 + 127。
  • mantissa:表示小数位,代表小数点后面的数值。

你可能会有点懵,没关系,我们通过例子来看看计算机如何存储十进制小数 10.625:

3

解读:存储过程分为三步,

  1. 十进制转换为二进制。
  2. 移位,类似科学计数法。
  3. 用 IEEE 754 标准存储浮点数。

IEEE 754 二进制浮点数转换为十进制小数

IEEE 754 定义了如下公式,将二进制浮点数转换为十进制小数:

4

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

5

用 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)

原文链接:底层揭秘:计算机如何存储浮点数

参考:

  1. https://0.30000000000000004.com/
  2. https://zhuanlan.zhihu.com/p/646206405
  3. https://zhuanlan.zhihu.com/p/102519285
  4. http://c.biancheng.net/view/314.html
Buy me a coffee~
室长 支付宝支付宝
室长 微信微信
0%