IEEE754规范: 四, 非规格数, ±infinity, NaN

第一章提到过, ieee754标准中, 浮点数包含三种状态

1. normal number(规格数) 

2. subnormal number(非规格数)

3. non-number(特殊数)

本章详细讲解这三种状态.


一. 首先, 如何区分这三种状态

其实这三种状态是通过指数部分区分的, 而且很容易区分.

以32位浮点数为例, 其内存状态分为3部分:

1位符号位     8位指数位     23位尾数位

其中, 如果8位指数位全为0, 就代表当前数是个非规格数. 或者说, 形如 * 00000000 *********************** 格式的数就是非规格数.

如果8位指数位全为1, 就代表当前数是个特殊数. 或者说, 形如 * 11111111 *********************** 格式的数就是特殊数.

如果8位指数不全为0, 也不全为1(也就是除去以上两种状态外, 剩下的所有状态), 这个数就是规格数.

随便几个例子: * 10101100 ***********************就是一个规格数

可见: 非规格数特殊数是两种特殊状态, 规格数则是非常常见的状态

示意图:

注意下图把特殊数分为了两种状态, 无穷大NaN:

深入理解计算机系统第三版

二. 这三种状态的作用

为什么要把浮点数分为这三种状态呢? 答案当然是有用啊, 而且作用相当直观:

规格数: 用于表示最常见的数值, 比如1.2, 34567, 7.996, 0.2. 但规格数不能表示0和非常靠近0的数.

非规格数: 用于表示0, 以及非常靠近0的数, 比如1E-38.

特殊数: 用于表示"无穷"和"NaN":

浮点数的存储和计算中会涉及到"无穷"这个概念, 比如:

32位浮点数的取值范围是, 如果你要往里面存储4e38(这超过了最大的可取值), 32位浮点数就会在内存中这样记录 "你存储的数超过了我的最大表示范围, 那我就记录你存储了一个无穷大..."

浮点数的存储和计算中还会涉及到"NaN (not a number)"这个概念, 比如:

你要给一个负数开根号(如 √-1), 但是ieee754标准中的浮点数却不知道该怎么进行这个运算, 它就会在内存中这样记录 "不知道怎么算, 这不是个数值, 记录为NaN"

可见, 这三种状态都是非常有用的, 作用也非常直观, 下面我们一个个来讲.


三. 状态1: 规格数

对于规格数: 

符号位, 1位: 可正可负

指数位, 8位: 不全为0, 且不全为1

对于32位浮点数来说, 规格数的指数位的取值范围是[1, 254], 偏置bias是127, 所以实际的指数是:

[1 - 127, 254 - 127], 即 [-126, 127]

注: 关于偏置, 可参见本系列第一章, 此处不再赘述

尾数位, 23位: 尾数位前隐藏的整数部分是1.  而非 0.

所以尾数位的取值范围是[1.00000000000000000000000, 1.11111111111111111111111] (二进制)

换算为10进制为[1,2)

注: 关于尾数位前隐藏的数, 可参见本系列第一章, 此处不再赘述

规格数的局限性: 无法表示 0 和 及其靠近0 的数

原因很简单, ieee754浮点数的求值公式是:

所以可求出32位浮点数的取值范围就是:

问题就出现在这里: 

注意尾数部分: 取值范围是[1, 2), 始终大于1

注意指数部分:, 这个数始终大于0, 即便2^-167非常小, 但还是大于0

那么: 一个始终大于1的数 * 一个始终大于0的数, 永远无法等于0

事实上, 1(尾数最小值) * 2^-167(指数最小值) = 2^-167.    2^-167就是当前我们能表示的最小值

也就是说: 使用规格数时, 我们除了无法表示0, 也无法表示(0, 2^-167)之间的, 靠近0的极小数...

这就是规格数的局限性, 这个局限性将由非规格数解决.


补充一点:

其实在本系列的第二章, 我们计算过32位浮点数的取值范围:

所以这里可以画一个示意图:

↑ 绿色区域就是32位浮点数中规格数的取值范围, 可见它取不到0和靠近0的极小数

↑ 红色区域包含0和靠近0的极小数, 红色区域其实是非规格数的取值范围, 见下一节.


四. 状态2: 非规格数

对于非规格数: 

符号位, 1位: 可正可负

指数位, 8位: 全为0

对于32位浮点数来说, 规格数的指数位全为0, 对应的值也是0. 偏置bias依旧是127, 但:

实际指数的计算方法是: 实际指数 = 1 - bias = 1 - 127 = -126, 即非规格数的实际指数固定为-126. 注意这是规定.

其实我们可以发现, 非规格数实际指数的计算方法(实际指数 = 1 - bias), 和规格数实际指数的计算方法(实际指数  = 指数位的值 - bias)不同

后文会看到这样规定的原因.

尾数位, 23位: 尾数位前隐藏的整数部分是0. 而非 1.

所以尾数位的取值范围是[0.00000000000000000000000, 0.11111111111111111111111] (二进制)

换算为10进制为[0,1)

非规格数的作用: 表示0和靠近0的数

那么规格数是怎么完成这个任务的呢. 

首先看看非规格数是怎么表示0的:

依旧要用到我们的ieee754浮点数求值公式:

然后, 非规格数尾数的取值范围是[0, 1), 指数固定为-126. 这就很简单了, 让尾数取0不就能表示数值0了:

可见当尾数取0时, 通过变更符号位, 我们可以表示出+0-0, IEEE754规范中也确实存在着这两种表示0的方式

注: 某些场景下, +0和-0会被认为完全相同, 某些场景下, +0和-0又被认为不完全相同. 这往往取决于具体的编程语言和应用场景, 此处不做讨论. 只需知道IEEE754中可以表示+0和-0即可. +0和-0在IEEE754中是两种内存状态(符号位不同)

然后看看非规格数是怎么表示接近0的数的:

准确来说, 我们要看看, 对于32位浮点数, 非规格数是怎么表示出之间的数的. 也就是如何表示出下图中的红色区域的:

其实也很简单:

浮点数求值公式:

然后, 非规格数尾数的取值范围是[0, 1), 指数固定为-126.

所以, 非规格数的取值范围就是:

等于:

 

等于:

这样就完成了...

现在我们尝试着把32位浮点数中的非规格数的取值范围, 和规格数的取值范围拼接在一起

32位浮点数中, 非规格数的取值范围:

32位浮点数中, 规格数的取值范围:

仔细看一下, 啊...非规格数的取值范围, 正好可以卡在规格数取值范围的中间, 现在我们得到了一个完整的取值范围:

感觉世界一下子清爽了起来. 

这就是非规格数的作用: 用于表示0和靠近0的数, 用于和规格数"珠联璧合", 形成一个完整的取值范围.

不过这还没有完...


五. 非规格数补充


1. 逐渐溢出

前文说过, 非规格数尾数的取值范围是[0, 1), 指数固定为-126

所以是尾数的变化在导致非规格数的值变大, 举例:

0 00000000 00000000000000000000001

就比

0 00000000 00000000000000000000000

要大一些

随着尾数逐渐增大, 相应的非规格数也在不断增大:

...

0 00000000 11111111111111111111111 这是非规格数的最大值

此时尾数(带上隐藏的整数部分0.)其实是0.11111111111111111111111, 是个比1小一点点的数, 不妨记做(1 - ε)

那, 此时非规格数的值就是 

好, 我们再往前前进一格, 此时会进入规格数的范围:

0 00000001 00000000000000000000000

这是个规格数, 

其尾数位的值: 其实隐藏了 1. 或者说, 此时真正的尾数应该是1.00000000000000000000000 , 也就是1

其指数位的值: 是1, 则实际指数应该是1 - bias = 1-127 = -126

所以这个规格数的值就是: , 这是规格数的最小值.

注意到没有: 非规格数的最大值是: 

规格数的最小值是: 

两者之间实现了非常平滑的过度, 非规格数的最大值非常紧密的连接上了规格数的最小值

非规格数 "一点点逐渐变大,  最后其最大值平稳的衔接上规格数的最小值", 这种特性在ieee754中被叫做逐渐溢出(gradual underflow)

明白了这一点, 就很容易想通: 

① 为什么规定非规格数的尾数前隐藏的整数部分是 0.  而规格数尾数前隐藏的整数部分是1. 

② 为什么非规格数的真实指数的计算公式是 1 - bias, 而规格数的真实指数的计算公式是 指数部分的值 - bias

仔细思考一下, 就是这些设计实现了逐渐溢出这种特性. 

↑ 关于第①点: 这使得非规格数的尾数取值范围是[0,1), 而规格数的尾数取值范围是[1,2), 两者平滑的衔接在了一起

↑ 关于第②点: 这使得对于32位浮点数来说, 非规格数的真实指数固定为-126, 而规格数的指数是[-126, 127], 两者也平滑的衔接在了一起...


2. 密集分布

第三章中我们说过: 如果把ieee754浮点数想象成一个表盘的话, 那表盘上的蓝点是越来越稀疏的. 或者说越靠近0越密集.

不过当时仅讨论了规格数的分布情况, 那非规格数呢.

答案是, 非规格数的蓝点分布间隔, 和规格数中蓝点最密集的区域(也就是最靠近0的区域)一致, 可以验证一下:

非规格数: 范围是 在这个范围中分布了2^23个蓝点, 则蓝点间的间隔是

规格数中蓝点最密集的区域, 也就是最靠近0的区域是: , 在这个范围中分布了2^23个蓝点, 则蓝点间的间隔是

所以, 即便把非规格数与规格数放在一起审视, ieee754浮点数表盘上的蓝点依旧是越靠近0越密集, 越靠近∞越稀疏

深入理解计算机系统第三版, 浮点数密度示意图. 中间部分密密麻麻的都是非规格数

下面是在c语言中的测试结果:

六. 状态3: 特殊数

特殊数分为两种: 无穷和NaN

1. 先说无穷

理解了非规格数, 再理解无穷就很简单了, 两者有很多相似之处:

对于无穷: 

符号位, 1位: 可正可负

指数位, 8位: 全为1

尾数位, 23位: 全部为0

当内存位于上述状态时, 就表示无穷(infinity)

具体写出来就是: * 11111111 00000000000000000000000 用于表示无穷(infinity)

其中符号位可正可负, 分别记做+infinity和-infinity


以32位浮点数为例, 其规格数的取值范围是: 

当要存储的数大于规格数取值范围的最大值时, 就会被记做+infinity, 比如2^128, 刚刚超过规格数的取值范围的最大值, 就会被记做+infinity

当要存储的数小于规格数取值范围的最小值时, 就会被记做-infinity, 比如-2^128, 刚刚小于规格数的取值范围的最小值, 就会被记做-infinity

需要注意的是: 所有+infinity的内存状态都是0 11111111 00000000000000000000000, 不会有任何变动

2^128对应的内存状态是0 11111111 00000000000000000000000

2^123456789对应的内存状态还是0 11111111 00000000000000000000000

同理, -infinity的内存状态都是1 11111111 00000000000000000000000

此外: 就像非规格数的最大值可以和规格数的最小值平稳衔接一样, 规格数的最大值也可以和+infinity平稳衔接:

规格数的最大值是: 0 11111110 11111111111111111111111

尾数位其实是1.11111111111111111111111, 非常接近2, 不妨记做2-ε

指数是127

所以最大值是: 

+infinity的内存状态则是: 0 11111111 00000000000000000000000

尾数其实是: 1.00000000000000000000000, 等于1

指数是128

所以+infinity的内存状态对应的值是: 

可见规格数的最大值也能和+infinity平稳衔接. -infinity同理.

现在我们就集齐了整个数轴:


↑ 而且各个节点都能平稳的衔接在一起


2. NaN

NaN则更简单, 前面说过, 如果计算出来的值不是一个数值, 则记录为NaN

NaN的内存状态是: 

符号位, 1位: 可正可负

指数位, 8位: 全为1

尾数位, 23位: 不全为0即可


仅仅是一种特殊状态标记而已.

需要注意的是, 根据wiki, 没有+NaN或-NaN这种说法, 统称为NaN

七. 总结

本章介绍了IEEE754规范中的非规格数, 特殊值(±infinity, NaN), 包括它们的内存状态, 作用, 工作原理等.

下一章会先偏离一下主线, 补充一些之前没有提到的琐碎知识点. 

下一章再见吧~

你可能感兴趣的:(IEEE754规范: 四, 非规格数, ±infinity, NaN)