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
改编自 2018 年 TensorFlow 开发者峰会上展示的培训性能幻灯片

本文主要以 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.10001210000010

在机器中存放的时候,需要忽略尾数的最高的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 的bias127
所以e的范围由Ebias决定。

E的范围是 1 1 1~ ( 2 n − 2 ) (2^n-2) (2n2),根据这个范围和上面的公式,可知指数e的范围是 ( 1 − 2 n ) (1-2^n) (12n)~ − 1 -1 1,也就是说,全是负的。加上偏移量bias之后,得到的阶码E就是正的了。这就是移码。

此外,你会发现E有两个数没有定义:E=0 和E= ( 2 n − 1 ) (2^n-1) (2n1),这也是为什么 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} 2n1)相加结果为8

小数部分0.01同样使用位置对应的次方,不过这里小数点后面第一位对应的就是 2 − 1 2^{-1} 21,如下:

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 00104对应的是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

Signaling NaN - From MathWorld

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐