0%

浮点数存储结构

浮点数在内存中的存储结构

基础概念

  • 十进制整数转二进制为除 2 取余,逆序排列
  • 十进制小数转二进制为乘 2 取整,顺序排列
  • 二进制转十进制是每位加权求和

\[ 11.1101_2=1*2^1+1*2^0+1*2^{-1}+1*2^{-2}+0*2^{-3}+1*2^{-4} \]

  • n 进制的科学计数法 \(a*n^b\) (\(0<|a|<n\)) 其中 a 和 b 都是 n 进制数字,类比十进制,b 表示 n 进制指数 (小数点移动的位数)

    E. G. 二进制 \(10000_2=1*2^{100_2}\), 指数相当于十进制的 4, 也就是小数点向右移动 4 位

浮点数国际标准 IEEE 754

国际标准 IEEE 754 标准使用一个三元组 \(\{S, E, M\}\) 表示一个浮点数 N

  • S 符号位,0 和 1 分别表示正和负

  • E 阶码,用移码表示

  • M 尾码,按照 IEEE 754 标准存储浮点数需要先规格化, 最后的尾码一定是 \((1.xxxxx)_2\) 的二进制格式,第一位一定是 1, 因此尾码存储时舍去了第 1 位的 1, 只保存了二进制小数部分,这样可以多表示一位

浮点数的规格化

同一个浮点数的表示规格并不统一,比如 \((1.11)_2\times 2^0 = (0.111)_2\times 2^1 = (0.0111)_2\times 2^2\).

为了数据的表示精度,就必须充分利用尾码的有效位数,IEEE 754 标准中当尾码大小不是 \((1.xxxx)_2\) 格式时就左右移动小数点并同时修改阶码大小,直到达到要求, 该过程称为浮点数规格化

浮点数的表示

浮点数中的指数 e 可能有正有负,为了方便表示,将 e 增加一个固定的偏移量得到移码 E 作为阶码

平常使用最多的是单精度 (32 位) 和双精度 (64 位) 浮点数, 尾码部分默认小数点前的一位 1 省去了,后面不再赘述

单精度浮点数

符号位 (S) 1 位,阶码 (E) 8 位,尾码 (M) 23 位

其中阶码偏移量为 127 (0x7F), 尾码仅表示小数部分, 前面的 1 和小数点省略

真值表示: \(X=(-1)^S\times (1.M) \times 2^{E - 127}\) \(e=E-127\)

由于 \(2^{23}=8388608\) 最大可表示精度为 7 位,但不能表示所有的 7 位数,可保证 6 位精度

双精度浮点数

符号位 (S) 1 位,阶码 (E) 11 位,尾码 (M) 52 位

其中阶码偏移量为 1023 (0x3FF), 尾码仅表示小数部分, 前面的 1 和小数点省略

真值表示: \(X=(-1)^S\times (1.M) \times 2^{E - 1023}\) \(e=E-1023\)

由于 \(2^{52}=4503599627370496\) 最大表示精度为 16 位,不能表示所有 16 位数字,保证精度为 15 位

特殊值

在特殊情况下,浮点数的计算不再按照常规的方式计算, 而是直接表示特殊的真值,以下对于单精度和双精度都一样

  • 真值 0: 当阶码 E 全 0, 且尾数 M 全 0 时,表示真值 X 为 0, 由于符号位不同, 所以有正负 0 两种表示

  • 无穷大:当阶码 E 全 1, 且尾码 M 全 0 时,表示真值为无穷大, 符号位不同可以表示正负无穷大

  • NaN: 当阶码全 1, 且尾码 M 非全 0 时,全都表示 NaN, NaN 没有正负之分, 符号位 0 或 1 都是 NaN

编码测试

编写一个函数,用于将数值以二进制打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename T>
void print_bin(T data) {
std::string str;
uint8_t* bytes = (uint8_t*)&data;
auto len = sizeof(T);
for (int i = len - 1; i >= 0; --i) {
uint8_t v = bytes[i];
for (int j = 8 - 1; j >= 0; --j) {
str.append((v & (0x1 << j)) > 0 ? "1" : "0");
}
str.append(" ");
}
printf("%s\n", str.c_str());
}

先看一下 1.0 的结果 print_bin(float(1.0)) 结果为 00111111 10000000 00000000 00000000

其中 S=0, \(E=(01111111)_2=127\), M=0

也就是 \(1.0=(-1)^S\times (1.M) \times 2^{E-127}=1\times 1.0 \times 2^0\)

再试试 0.5, print_bin(float(0.5)) 结果为 00111111 00000000 00000000 00000000 与 1.0 的区别仅仅是阶码小了 1

真值计算 \(0.5=(-1)^(0)\times (1.0) \times 2^{126-127}=1\times 1.0 \times 2^{-1}\)

再验证一下特殊值是否如我们所想

1
2
3
4
5
6
7
8
9
10
11
12
printf("%f\n", std::numeric_limits<float>::infinity());
print_bin(std::numeric_limits<float>::infinity());
printf("%f\n", -std::numeric_limits<float>::infinity());
print_bin(-std::numeric_limits<float>::infinity());
printf("%f\n", std::numeric_limits<float>::quiet_NaN());
print_bin(std::numeric_limits<float>::quiet_NaN());
printf("%f\n", -std::numeric_limits<float>::quiet_NaN());
print_bin(-std::numeric_limits<float>::quiet_NaN());
float nan = std::numeric_limits<float>::infinity();
*(uint32_t*)(&nan) |= (1 << 3);
printf("%f\n", nan);
print_bin(nan);

输出结果如下

1
2
3
4
5
6
7
8
9
10
inf
01111111 10000000 00000000 00000000
-inf
11111111 10000000 00000000 00000000
nan
01111111 11000000 00000000 00000000
nan
11111111 11000000 00000000 00000000
nan
01111111 10000000 00000000 00001000

可以发现,对于无穷大,printf 以字符串 inf 表示了, quiet_NaN() 获取的 NaN 值是阶码全 1, 尾码的第一位设置为 1, 改变符号位依然是 NaN, 我试了一下在无穷大的基础上, 将尾码倒数第 4 位 (随机一位) 设置为 1, 输出为 NaN, 符合预期