第一章提到过, 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), 包括它们的内存状态, 作用, 工作原理等.
下一章会先偏离一下主线, 补充一些之前没有提到的琐碎知识点.
下一章再见吧~