IEEE 754-2008 浮点数格式详解

最近研究了一波浮点数标准,发现 2008 年的标准增加了三个十进制浮点数的标准: decimal32 decimal64 和 decimal128 ,但是我上网搜遍都找不到相关的中文资料,英文资料也少得可怜,独自靠着标准文档和维基百科可怜兮兮地研究了半天才搞明白,特此记录一下。

几个常量的定义

  • b ,基数 (base) ,下文中只可能为 2 或 10 。
  • p,精度 (precision) 。
  • emax ,指数的最大可能值。
  • emin ,指数的最小可能值,对于所有类型 emin=1-emax

对于每种浮点格式,均可能有以下的数值:

  • 有符号零或非零数字 (-1)^s\times b^e\times m,其中
    • s 为 1 或 0 。
    • e 为任何 emin\le e\le emax 的值。
    • m 是一个由数字串 \overline{d_0.d_1d_2\cdots d_{p-1}}_{(b)} 表示的数字,其中又有 0\le d_i<b (因此 0\le m<b )。
  • 两个无穷量 +\infty-\infty
  • 两种非数字 (NaN, Not a Number) 。quiet NaN 和 signaling NaN 。

二进浮点格式

不管怎样,还是先从经典的二进制浮点数开始。作为现在主流的浮点数格式,二进浮点的资料网上一搜一大把(我搜十进浮点相关资料的时候出来的全部都是二进浮点格式的资料),所以本文的二进制浮点格式以复习为主。

如图,二进浮点格式由以下三部分组成:

  1. 1-bit 符号位 S
  2. w-bit 指数 E=e+bias
  3. t-bit 尾数 t=p-1, T=\overline{d_1d_2\cdots d_{p-1}}_{(2)} ,首位 d_0 默认不出现。

以下是标准定义的几种二进浮点格式的各项参数:

参数 binary16 binary32 binary64 binary128 binary{k}
k 16 32 64 128 k, 32|k
p 11 24 53 113 k-\mathrm{round}(4\log_2 k)+13
emax 15 127 1023 16383 2^{k-p-1}-1
bias 15 127 1023 16383 emax
sign bit 1 1 1 1 1
w 5 8 11 15 \mathrm{round}(4\log_2 k)-13
t 10 23 52 112 k-w-1

规格化浮点数

规格化浮点数 (normalized number) 是指 E_0\cdots E_{w-1} 不全为 0 或不全为 1 的情况,这时候浮点数的值由公式

\displaystyle{(-1)^S\times 2^{E-bias}\times m}

给出,其中 S, E, T 的值均直接由相应的块的二进制整数值表示。 m 的值由以下公式推导出

\displaystyle{m=1+2^{-t}T=1+\sum_{i=1}^t2^{-i}d_i}

非规格化浮点数

非规格化浮点数 (subnormalized number) 是指 E_0\cdots E_{w-1} 全为 0 的情况。因为 0\le T<2^t, 1\le m<2 ,规格化浮点数无法表示 0 ,因此规定:当一个数字的绝对值小于 b^{emin} 时转为非规格化表示,浮点数的值由公式

\displaystyle{(-1)^S\times2^{emin-t}T=(-1)^S2^{emin}\sum_{i=1}^t2^{-i}d_i}

给出。

此时 0 的表示即为除了符号位之外其它比特全为 0 。

特殊值

特殊值包括无穷大和 NaN ,无穷大的表示为 E 全为 1 ,尾数 T 全为 0 ,其它情况均为 NaN 。

当数字是为 NaN 时,若 d_0=0 则 NaN 为 quiet NaN ,即 FPU 不会产生硬件异常,否则产生异常。

十进浮点格式

热身结束,接下去是反人类的十进浮点格式。

为什么说反人类呢,首先在十进浮点格式中,标准提供了两种编码方案,一种叫十进制编码方案,一种叫二进制编码方案,两种编码可以说完全不同,而且除了这个以外,坑爹的组合部分的编码也是很反人类的。

组成部分和参数

十进浮点数的组成部分包括:

  1. 1-bit 符号位 S
  2. 5-bit 组合部分,这部分的解析比较复杂,待下文细述。
  3. w-bit 指数延续部分。
  4. t-bit 尾数延续部分。

此图是十进制编码方式示意图,注意此图记号和图示和 IEEE 754-2008 上的不完全相同,是经过我自己稍加修改后的,如果同时在参照 IEEE 754-2008 注意转换。

以下是标准定义的几种十进浮点格式的各项参数:

参数 decimal32 decimal64 decimal128 decimal{k}
k 32 64 128 k, 32|k
p 7 16 34 9k/32-2
emax 96 384 6144 3\times2^{(k/16+3)}
bias 101 398 6167 emax+p-2
sign bit 1 1 1 1
w 6 8 12 k/16+4
t 20 50 110 15\times k/16-10

组合部分解析

这部分在标准里讲得非常崎岖,让我看得死去活来。

十进制编码方案的组合部分解析总结起来就是下面三条,颜色和上面的图相对应,表示最终属于哪个部分:

  • {\color{orange}G_0G_1}{\color{green}G_2G_3G_4}G_0G_1为指数部分最高两位, \overline{G_2G_3G_4}_{(2)} 组成尾数的最高有效数字。
  • \;1\;\,\;1\;\,{\color{orange}G_2G_3}{\color{green}G_4}G_2G_3为指数部分最高两位, 8_{(10)}+{G_4}_{(2)} 组成尾数的最高有效数字。
  • \;1\;\,\;1\;\,\;1\;\,\;1\;\,G_4 ,特殊值,无穷大或 NaN 。

二进制编码方案中,上图中的 comb 和 exponent 部分才是真正的组合部分, exponent 的具体位置由组合部分的前两个字节决定。因此无视上图,这里包括符号位从 MSB 开始用 b_0\cdots b_{k-1} 编号,编码满足:

  • \overline{b_1b_2}_{(2)}\not=11_{(2)} 时,\overline{b_1b_2} 和其后的 w 位组成指数部分,剩下为尾数部分。
  • \overline{b_1b_2}_{(2)}=11_{(2)}, \overline{b_3b_4}_{(2)}\not=11 时, b_3b_4 和其后的 w 位组成指数部分,剩下为尾数部分。
  • 特殊值和十进制编码方案相同。

指数部分解析

指数部分由组合部分的两个比特和剩余的 w 个比特组成,由于组合部分的两个比特不可能为 11 ,组合部分中的指数部分共 3 种取值。因此指数的所有可能取值总计有 3\times2^w 种。

十进浮点数的指数部分也和二进浮点数的指数部分一样,是由二进制表示的 E 减去一个置偏值 bias 。可能会有人(比如我)会奇怪为什么置偏值不是恰好是 3\times2^{w-1} ,这是因为此处的指数直接和 T 相乘,和二进浮点数乘以规格化的 T 不同,这里没有规格化浮点数,因此置偏值比 3\times2^{w-1} 多出来的部分就相当于规格化后多乘的值,这也是那个 bias=emax+p-2p-2 的由来。

DPD 编码

在讲尾数部分的解析前必须要先了解一下 DPD 编码(尽管最流行的 x86-64 架构上的实现用的是二进制编码)。

在十进浮点数的十进制编码中,为了在尾数部分使用十进制,使用了一种从十进制和二进制相互映射的编码,这种编码可不是 8421BCD 这种小儿科编码——太浪费空间了。对于新的编码,我们希望找到一个二的正整幂,使得比它小的最接近它的十的正整幂和它比值尽可能接近 1 ,这样才能节约空间。另一方面这个二的正整幂也要尽可能的小,这样粒度才能小,更容易给空间较小的浮点格式分配空间。

很容易想到的数字是 1024 ,没错,这个数字确实是很令人满意的,标准也采用了这个数,因为接下去一个比 1024 更优的数字是 10141204801825835211973625643008 ,是 2 的 103 次方。

DPD 编码全称 Densely Packed Decimal Encoding,直译就是紧凑十进制编码,它也是一个 IEEE 标准,将 10 个比特映射到 0~999 中,借用维基百科的表格解释一下这个编码:

DPD encoded value   Decimal digits
b9 b8 b7 b6 b5 b4 b3 b2 b1 b0 d2 d1 d0 Values encoded Description
a b c d e f 0 g h i 0abc 0def 0ghi (0–7) (0–7) (0–7) Three small digits
a b c d e f 1 0 0 i 0abc 0def 100i (0–7) (0–7) (8–9) Two small digits,
one large
a b c g h f 1 0 1 i 0abc 100f 0ghi (0–7) (8–9) (0–7)
g h c d e f 1 1 0 i 100c 0def 0ghi (8–9) (0–7) (0–7)
g h c 0 0 f 1 1 1 i 100c 100f 0ghi (8–9) (8–9) (0–7) One small digit,
two large
d e c 0 1 f 1 1 1 i 100c 0def 100i (8–9) (0–7) (8–9)
a b c 1 0 f 1 1 1 i 0abc 100f 100i (0–7) (8–9) (8–9)
x x c 1 1 f 1 1 1 i 100c 100f 100i (8–9) (8–9) (8–9) Three large digits

怕了吗,这里 b9 到 b0 为编码后的 10 个比特的二进制值,右边的 d2 d1 d0 对应着解码后十进制的三个数字。

拿第三行的编码解码来解释一下,当 DPD 编码后的二进制 {b_3}_{(2)}=1_{(2)},\overline{b_2b_1}_{(2)}=01_{(2)} 的时候,十进制表示为 \overline{0b_9b_8b_7}_{(2)}\times100_{(10)}+\overline{100b_4}_{(2)}\times10_{(10)}+\overline{0b_6b_5b_0}_{(2)}

尾数部分解析

尾数部分的最高位在组合部分中,剩余部分在最后的 t 个比特中。

如果采用十进制编码方式,将 t 拆成十个十个一组,每组解码出 3 个十进制整数,和组合部分的一个数字拼接成一个十进制的数字,因此十进制编码方式的最大值为 (10^{3t/10+1}-1)\times10^{emax-p+1}

如果采用二进制编码方式, t直接按照二进制解析,但是我在 x86-64 平台下测试后发现虽然明显二进制编码下可以再继续存储更大的数字,但是只要达到 (10^{3t/10-1}-1)\times10^{emax-p+1} 再加上一个 10^{emax-p+1} 就会导致溢出,也就是说上限和十进制编码方式相同。对应具体的标准要求我没找到。

由以上可以得出,整个十进浮点数的值应由下列公式得出:

\displaystyle{(-1)^S\times T\times10^{E-bias}}

这里 TE 的值都是由组合部分和各自延续部分共同计算得出来的。

另外,由于十进制中没有规格化浮点数的概念,所以同一个值可以有多种形式的表达,如 1\times10^0=10\times10^{-1}=\cdots=10^{p-1}\times10^{1-p}  。

特殊值

和二进浮点数类似,当组合部分前 4 个比特均为 1 的时候该浮点数表示一个特殊值。其中,第 5 个比特为 0 则表示为无穷大,第 5 个比特为 1 则表示 NaN 。和二进浮点数类似的,第 5 个比特之后的一个比特如果为 0 则为 quiet NaN ,否则为 signaling NaN 。

后记

标准中还提到了扩展浮点数格式和可扩展浮点数格式,因为没有给出具体要求,只有最低精度和 emax 限制,具体到实现上可以千差万别,所以本文中没有提及。

GNU 在部分平台的 C 编译器里默认支持了三种十进浮点数格式 _Decimal32 _Decimal64 和 _Decimal128 ,这其实是三个 typedef 。在 C++ 编译器中,用类的形式在头文件 <decimal/decimal> 中提供了 std::decimal::decimal32 std::decimal::decimal64 和 std::decimal::decimal128 ,但是没有提供 C 中的三个类型声明(但是这三个类型是存在的,实际上是加了 __attribute__ 的 float  ,也因此造成了下面这个问题)。

但是 C++ 中的几个类是存在问题的, g++ 采用了透明类的机制提供了这三个类,也就是任何可以传入 _DecimalN 的地方均可传入 std::decimal::decimalN ,为此它们的底层 ABI 是完全相同的,另一方面,带有 df dd 或 dl 的字面量类型实际上是 C 中的三种类型,结果事与愿违:如果一个模板函数同时接受了 C 中的 _DecimalN 和 C++ 中的 std::decimal::decimalN 的话,会导致 ABI 冲突,编译可以通过,但是链接就挂了。

因此如果需要同时使用字面量和类,建议自己重新用类包装一下 C 中的三个类型,如 _DecimalN 的声明如下:

typedef float decimal32 __attribute__((mode(SD)));
typedef float decimal64 __attribute__((mode(DD)));
typedef float decimal128 __attribute__((mode(TD)));

顺便一提,为了方便自己理解浮点数的解析,我写了个简单的库,有时间的话包装一下扔我 github 上。

发表评论

电子邮件地址不会被公开。 必填项已用*标注