IEEE 浮点数结构

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

基础概念

  • 十进制整数转二进制为除 2 取余, 逆序排列
  • 十进制小数转二进制为乘 2 取整, 顺序排列
  • 二进制转十进制是每位加权求和
$$ 11.1101_2=1\times2^1+1\times2^0+1\times2^{-1}+1\times2^{-2}+0\times2^{-3}+1\times2^{-4} $$
  • n 进制的科学计数法 $a*n^b$ ($0<|a|<n$) 其中 a 和 b 都是 n 进制数字, 类比十进制, b 表示 n 进制指数 (小数点移动的位数)

    例: 二进制 $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 作为阶码

移码 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
15
16
17
18
19
20
21
const std = @import("std");

fn printBits(value: anytype) void {
    const T = @TypeOf(value);
    const bit_width = @bitSizeOf(T);
    const UnsignedT = std.meta.Int(.unsigned, bit_width);
    const bits: UnsignedT = @bitCast(value);
    std.debug.print("{d:<5} {s:<5}: ", .{ value, @typeName(T) });
    var i: usize = 0;
    while (i < bit_width) : (i += 1) {
        const ShiftType = std.math.Log2Int(UnsignedT);
        const shift_amt = @as(ShiftType, @intCast(bit_width - 1 - i));
        const bit = (bits >> shift_amt) & 1;
        std.debug.print("{d}", .{bit});
        // 每8位打印一个空格 (除了最后一位)
        if ((i + 1) % 8 == 0 and (i + 1) < bit_width) {
            std.debug.print(" ", .{});
        }
    }
    std.debug.print("\n", .{});
}

先看一下1.0的结果 printBits(@as(f32, 1.0)) 结果为 1 f32 : 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, printBits(@as(f32, 0.5)) 结果为 0.5 f32 : 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
13
14
15
pub fn main() void {
    printBits(@as(f32, 0.0));
    printBits(-@as(f32, 0.0));
    printBits(@as(f32, 1.0));
    printBits(@as(f32, 0.5));
    printBits(std.math.inf(f32));
    printBits(-std.math.inf(f32));
    printBits(std.math.nan(f32));
    printBits(-std.math.nan(f32));
    var nan_test = std.math.nan(f32);
    var nan_u32: u32 = @bitCast(nan_test);
    nan_u32 = (nan_u32 | (1 << 3));
    nan_test = @bitCast(nan_u32);
    printBits(nan_test);
}

输出结果如下

1
2
3
4
5
6
7
8
9
0     f32  : 00000000 00000000 00000000 00000000
-0    f32  : 10000000 00000000 00000000 00000000
1     f32  : 00111111 10000000 00000000 00000000
0.5   f32  : 00111111 00000000 00000000 00000000
inf   f32  : 01111111 10000000 00000000 00000000
-inf  f32  : 11111111 10000000 00000000 00000000
nan   f32  : 01111111 11000000 00000000 00000000
-nan  f32  : 11111111 11000000 00000000 00000000
nan   f32  : 01111111 11000000 00000000 00001000

可以发现, 无穷大的阶码全1, 尾码全0, 改变符号位可得到正/负无穷大; NaN阶码全1, 尾码非0, 改变符号位依然是NaN, 试了一下在无穷大的基础上, 将尾码倒数第4位(随机一位)设置为1, 输出为NaN, 符合预期

hugo + stack 构建
使用 Hugo 构建
主题 StackJimmy 设计