Float 的存储结构

问题导入

1
2
3
4
5
In [1]: 0.1 + 0.2
Out[1]: 0.30000000000000004

In [2]: 0.3 + 0.2
Out[2]: 0.5

简介

python 的 float 类型是由 C 语言的 double 类型实现的,double 类型对应 IEEE754 中的双精度实现方法。 double 类型的存储占用的空间大小在 64 位的机器上为 8 个字节,即 64 个比特位,其中 1 位为符号位,11 位为指数位,52 位为尾数位,如下图所示

浮点数的存储分为两种类型,单精度浮点数和双精度浮点数,分别对应 IEEE754 中的 float 和 double 类型。其中 float 类型占用 4 个字节,即 32 个比特位,double 类型占用 8 个字节,即 64 个比特位。

IEEE754 中,关于双精度浮点数存储定义为 1 位符号位,11 位指数位,52 位尾数位,如下图所示

1
2
3
4
5
6
7
8
9
10
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+---------------------+---------------------------------------+
|S| Exponent | Fraction |
|I| (11 bits) | (52 bits) |
|G| | |
|N| | |
+-+---------------------+---------------------------------------|
| Fraction (52 bits) |
+---------------------------------------------------------------|
  • sign:符号位,0 代表正数,1 代表负数
  • exponent:指数位,用于表示浮点数的指数部分,用移码表示,即真实值加上 1023
  • fraction:尾数位,用于表示浮点数的尾数部分,用二进制表示,最高位默认为 1,即 1.xxxxxx,所以只需要存储小数点后面的 52 位即可

IEEE754 中,关于单精度浮点数存储定义为 1 位符号位,8 位指数位,23 位尾数位,如下图所示

1
2
3
4
5
6
7
8
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+---------------+---------------------------------------------+
|S| Exponent | Fraction |
|I| (8 bits) | (23 bits) |
|G| | |
|N| | |
+-+---------------+---------------------------------------------|
  • 使用 4 个字节,32 位进行存储
  • sign 符号位,占用一个位,0 代表正数,1 代表负数
  • exponent 指数位,表示浮点的指数位,真实值加上 127,占用 8 个位
  • fraction 尾数位,标识浮点的尾数部分,最高位默认为 1,即 1.xxxxxx,所以只需要存储小数点后面的 23 位即可

生成方法

  1. 判断符号位,如果是负数,将符号位设置为 1,否则设置为 0
  2. 将整数部分转换为二进制
  3. 将小数部分不断乘以 2,取整数部分,直到小数部分为 0
  4. 将整数和小数部分拼接起来,然后进行规格化,即小数点左移或者右移,使得小数点左边只有一个 1
  5. 最后将符号位、指数位和尾数位拼接起来,即为最终的二进制表示

末尾位舍入模式

任何有效数上的运算结果,通常都存放在较长的寄存器中,当结果被放回浮点格式时,必须将多出来的比特丢弃。 有多种方法可以用来运行舍入作业,实际上IEEE标准列出4种不同的方法:

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

举例

0.1 值双精度类型的存储计算方法

  1. 0.1 是正数,所以符号位为 0

  2. 整数部分为 0,所以整数部分的二进制为 0

  3. 小数部分为 0.1,不断乘以 2,取整数部分,直到小数部分为 0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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
    ...

    所以小数部分的二进制为 000 1100 1100 1100 ...

  4. 将整数和小数部分拼接起来,得到 0.0001100110011001100110011001100110011001100110011001100,然后进行移位,使得小数点左边只有一个 1,即 1.100110011001100110011001100110011001100110011001100,所以指数位为 -4,即 -4 + 1023 = 1019,所以指数位为 011 1111 1011

  5. 最后得到的二进制表示为 0 011 1111 1011 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001,后面还存在的数为 1001 1001,故根据舍入规则算最接近的数应该加 1,所以最后四位变为 1001 + 1 = 1010

  6. 最后得到 16 进制表示为 3fb999999999999a

验证

1
2
3
4
5
6
7
8
9
10
11
12
import struct

def float_to_hex(f):
# 将浮点数转换为字节表示
# d 代表 double 类型
# f 代表 float 类型
# > 表示大端字节序
# < 表示小端字节序
bytes_representation = struct.pack('>d', f)
# 将字节表示转换为十六进制字符串
hex_representation = ''.join(format(b, '02x') for b in bytes_representation)
return hex_representation
1
2
In [3]: float_to_hex(0.1)
Out[3]: '3fb999999999999a'

还原

按照拼接的方法进行组合将其拆为 0 011 1111 1011 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

第一位为 0,表示正数

指数位为 011 1111 1011,即 1019,减去 1023 得到 -4

即尾数位需要向左移动四个位获得

0 000 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101 0

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
0 = 0 * 2^-1 = 0
0 = 0 * 2^-2 = 0
0 = 0 * 2^-3 = 0

1 = 1 * 2^-4 = 0.0625
1 = 1 * 2^-5 = 0.03125
0 = 0 * 2^-6 = 0
0 = 0 * 2^-7 = 0

1 = 1 * 2^-8 = 0.00390625
1 = 1 * 2^-9 = 0.001953125
0 = 0 * 2^-10 = 0
0 = 0 * 2^-11 = 0

1 = 1 * 2^-12 = 0.000244140625
1 = 1 * 2^-13 = 0.0001220703125
0 = 0 * 2^-14 = 0
0 = 0 * 2^-15 = 0

1 = 1 * 2^-16 = 0.0000152587890625
1 = 1 * 2^-17 = 0.00000762939453125
0 = 0 * 2^-18 = 0
0 = 0 * 2^-19 = 0

1 = 1 * 2^-20 = 0.00000095367431640625
1 = 1 * 2^-21 = 0.000000476837158203125
0 = 0 * 2^-22 = 0
0 = 0 * 2^-23 = 0

1 = 1 * 2^-24 = 0.000000059604644775390625
1 = 1 * 2^-25 = 0.0000000298023223876953125
0 = 0 * 2^-26 = 0
0 = 0 * 2^-27 = 0

1 = 1 * 2^-28 = 0.0000000037252902984619140625
1 = 1 * 2^-29 = 0.00000000186264514923095703125
0 = 0 * 2^-30 = 0
0 = 0 * 2^-31 = 0

1 = 1 * 2^-32 = 0.00000000023283064365386962890625
1 = 1 * 2^-33 = 0.000000000116415321826934814453125
0 = 0 * 2^-34 = 0
0 = 0 * 2^-35 = 0

1 = 1 * 2^-36 = 0.000000000014551915228366851806640625
1 = 1 * 2^-37 = 0.0000000000072759576141834259033203125
0 = 0 * 2^-38 = 0
0 = 0 * 2^-39 = 0

1 = 1 * 2^-40 = 0.0000000000009094947017729282379150390625
1 = 1 * 2^-41 = 0.00000000000045474735088646411895751953125
0 = 0 * 2^-42 = 0
0 = 0 * 2^-43 = 0

1 = 1 * 2^-44 = 0.00000000000005684341886057758331298828125
1 = 1 * 2^-45 = 0.000000000000028421709430288791656494140625
0 = 0 * 2^-46 = 0
0 = 0 * 2^-47 = 0

1 = 1 * 2^-48 = 0.0000000000000035527136788005008697509765625
1 = 1 * 2^-49 = 0.00000000000000177635683940025043487548828125
0 = 0 * 2^-50 = 0
1 = 1 * 2^-51 = 0.0000000000000000888178419700125217437744140625

0 = 0 * 2^-52 = 0

加起来为 0.1000000000000000055511151231257827021181583404541015625

问题解答

0.1 + 0.2 = 0.30000000000000004

  • 0.1 的二进制表示为 0.0001100110011001100110011001100110011001100110011001100
  • 0.2 的二进制表示为 0.0011001100110011001100110011001100110011001100110011010
  • 相加后得到值为 0.0100110011001100110011001100110011001100110011001100110
  • 0.3000000000000000444089209850062616169452667236328125

0.3 + 0.2 = 0.5

  • 0.3 的二进制表示为 0.0100110011001100110011001100110011001100110011001100110
  • 0.2 的二进制表示为 0.0011001100110011001100110011001100110011001100110011010
  • 相加后得到值为 0.1000000000000000000000000000000000000000000000000000000
  • 0.5