IEEE 754 标准是现在主流的浮点数标准,除了常见语言中的 FP32、FP64 之类的类型,还有一些为了量化、加速深度学习的 FP8 类型(E4M3、E5M2)也是使用 IEEE 754 标准来定义无穷等含义。此外,通过了解和学习,在研究其他浮点数格式的时候也会有帮助,比如机器学习使用的 BFLOAT16。
如果你是为了考试,那么本文可能不太适合你,因为教材上使用的 IEEE 754 和 2008 往后版本的区别不少。
下图是 FP32、FP16、BF16 三种浮点格式对比(图自BFloat16: The secret to high performance on Cloud TPUs - Google Cloud)
本文主要以 IEEE 754 的 FP32 为例,展示结构和如何得到浮点数。
数据结构
IEEE 754 中,浮点数的结构为:
结构: |符号|阶码|尾数|
FP32: |1位|8位|23位|
FP64: |1位|11位|52位|
举个例子, − 12.2 5 3 -12.25^3 −12.253,基数(基值)为 2(也就是使用二进制表示):
- 符号为
-
,也就是1
; - 尾数为
12.25
,也就是1100.01
。
IEEE 中没有记录小数点位置的地方,也就是可以节约这些空间。如何做到这点的呢?这就是规格化。
也就是强制小数点在第一个1
后面。于是可以得到
1.10001
1.10001
1.10001x
2
3
2^{3}
23
- 阶码就是上面这个数中的
指数部分+偏移量
。FP32 偏移量为 127,3+127=130
的二进制为1000 0010
这时候得到
−
12.2
5
3
-12.25^3
−12.253的二进制表达为:
−
1.10001
∗
2
10000010
-1.10001*2^{1000 0010}
−1.10001∗210000010
在机器中存放的时候,需要忽略尾数的最高的1
,因为规格化要求小数点必须在第一个1
后面。也就是说小数点前面肯定有个1
,可以省下来一位,提高精度:
| 符号1位 | 阶码8位 | 尾数23位 |
1 1000 0010 1000 1000 0000 0000 0000 0000
偏移量
这时候你可能好奇计算阶码的时候偏移量是什么,127
是哪来的?
来看看文档(这个表格非常不错,如果你能理解每一个单元的含义,相信你也就真的懂了 IEEE 754。从中你也可以看到所谓的“阶码”、“尾数”的含义,我是没想到“尾数”居然是从英文翻译过来的):
会发现偏移(bias)就是“emax, maximum exponent e”,也就是最大指数e
,计算方法其实是:
2 总位数 − 尾数 − 1 2^{总位数-尾数-1} 2总位数−尾数−1
为什么要有个偏移呢?
我在《原码、补码、反码、移码是什么?》- ZhongUncle’s CSDN中介绍移码的时候说到,它的含义就是“给数加上一个偏移数后,使其具有非负的表达形式”。
接下来我来解释这句话
下面
e
为指数,E
为阶值。
E=e+bias
。
FP32 的bias
为127
。
所以e
的范围由E
和bias
决定。
E
的范围是
1
1
1~
(
2
n
−
2
)
(2^n-2)
(2n−2),根据这个范围和上面的公式,可知指数e
的范围是
(
1
−
2
n
)
(1-2^n)
(1−2n)~
−
1
-1
−1,也就是说,全是负的。加上偏移量bias
之后,得到的阶码E
就是正的了。这就是移码。
此外,你会发现E
有两个数没有定义:E
=0 和E
=
(
2
n
−
1
)
(2^n-1)
(2n−1),这也是为什么 FP32 的阶值E
有 8 位,但是最大值是127
,而不是255
,是因为有一位用来表达其他的内容了。
阶值和尾数全为1或0的含义
教材中的IEEE 754(早期版本)
在某些教材中,会告诉你阶码自己也有个符号位,这么记也好。而且很多计算题都是这么出的。
| 1位 | 8位 |23位|
| 符号 |阶符 | 阶码|尾数|
这里阶码全是1
的情况就要讨论了:
- 如果此时尾数非零,符号位任意,那么为
NaN
(不分两种); - 如果此时尾数为零,符号位为
0
(正),那么表示正无穷大; - 如果此时尾数为零,符号位为
1
(负),那么表示负无穷大;
这里阶码全是0
的情况就要讨论了:
- 如果此时尾数非零,符号位任意,那么为非规格化数,这里表示的阶值为
-126
,而且节约掉的小数点前那位是0
; - 如果此时尾数为零,符号位为
0
(正),那么表示正零; - 如果此时尾数为零,符号位为
1
(负),那么表示负零;
IEEE 754 2019
下面这段内容是我在 IEEE 754 2019 上看到的,早期的版本(2008 之前)似乎没有这个定义或者不明确,如果你要考试就别看了,容易干扰。
不论后面尾数是什么,阶码1111 111x
前面的1111 111
是表示NaN
,也就是无定义的数。
如果最后一位为1
,则是sNaN
(signaling NaN),否则为qNaN
(quiet NaN)。二者的区别在于“signaling(信号)”,前者会发出一些浮点异常信号,所以经常在异常和处理机制中使用,后者很少发出浮点异常信号,就是“安静”。
浮点十进制转二进制
浮点数十进制转二进制的时候,需要分别考虑整数和小数部分。
以8.25
为例。
整数部分8
:
2|__8__ ----0
2|__4__ ----0
2|__2__ ----0
1 ----1
然后倒着取上面右列的结果,为1000
。
小数部分0.25
:
0.25
x 2
------
0.50 ----整数部分为0
x 2
------
1.00 ----整数部分为1
按顺序取结果的整数部分,也就是01
。
合起来就是1000.01
。
浮点二进制转十进制
反过来转换方法如下。
以1000.01
为例。
整数部分1000
:
1 0 0 0
2^3 2^2 2^1 2^0
1x8+0x4+0x2+0x1=8
按位置对应的次方(
2
n
−
1
2^{n-1}
2n−1)相加结果为8
。
小数部分0.01
同样使用位置对应的次方,不过这里小数点后面第一位对应的就是
2
−
1
2^{-1}
2−1,如下:
0 1
2^(-1) 2^(-2)
0x0.5 + 1x0.25 = 0.25
结果为0.25
。
整数和小数部分相加,得到8.25
。
浮点的加减计算
浮点数的运算时分开进行阶码和尾数运算。
对齐小数点
上面规格化的时候,我们强制让第一个1
出现在小数点左边,所以不能直接尾数和尾数、阶数和阶数运算。比如
3.2
3.2
3.2x
1
0
3
10^3
103和
1.2
1.2
1.2x
1
0
4
10^4
104就不能直接各部分相加。
所以我们要对齐小数点,换句话说就是对齐阶数。比如上面的 1.2 1.2 1.2x 1 0 4 10^4 104变成 12 12 12x 1 0 3 10^3 103就可以各部分相加了。
二进制的时候,由于阶数是倒着数的,所以小的一个加一个数就和大的一样了,或者大的减一个数就和小的一样。
比如指数3
对应阶数为1000 0010
,4
对应的是1000 0011
,可以看到前者加一就是后者。
在对齐小数点的时候,也要对尾数进行操作,方法就是右移(或者乘 2,对于二进制无符号数来说,这两个操作是等价的,右移更快,因为位移的电路比乘法的“便宜”)。
加减尾数
阶数对齐之后,注意还有规格化忽略掉的一位,还原那一位之后,就可以直接加减尾数了。
再次规格化
在计算完之后,需要对结构再次规格化。如果在规格化的时候,有位超出了规定位数,就要舍去了。方法有四种:
- 0 舍 1 入(二进制版“四舍五入”);
- 正向舍入:取大于自己的第一个可取数;
- 负向舍入:取小于自己的第一个可取数;
- 截断法:直接舍去超出的部分。
判断指数是否溢出
计算完之后要判断结果的指数是否超出范围,也就是是否溢出。
以 FP32 为例:
- 如果指数超出 127,那么发生上溢,产生一个异常;
- 如果指数小于 127,那么发生下溢,通常直接当
0
。
希望能帮到有需要的人~
参考资料
IEEE 754-2019 - IEEE Standard for Floating-Point Arithmetic:2019年的新文档
IEEE 754-1985 - IEEE Standard for Floating-Point Arithmetic
:1985年的老文档。
BFloat16: The secret to high performance on Cloud TPUs - Google Cloud