IEEE 754浮点数构成与转换
IEEE 754 标准是现在主流的浮点数标准,除了常见语言中的 FP32、FP64 之类的类型,还有一些为了量化、加速深度学习的 FP8 类型(E4M3、E5M2)也是使用 IEEE 754 标准来定义无穷等含义。此外,通过了解和学习,在研究其他浮点数格式的时候也会有帮助,比如机器学习使用的 BFLOAT16。下图是 FP32、FP16、BF16 三种浮点格式对比(图自本文主要以 IEEE 754
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.25 -12.25 −12.25,基数(基值)为 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.25
-12.25
−12.25的二进制表达为:
−
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
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)