浮点数因为它的独特的表示方法,造成了比整数表示复杂的多的情况。而在程序中却不得不经常跟浮点数打交道。最近在看《深入理解计算机系统》,于是就想把与浮点数相关的东西整理出来,方便以后翻阅。
在初高中学过的科学计数法是一种很美妙的表示方法,它可以很容易地表示很小的数和很大的数。下面就是一个十进制数的科学计数法表示:
浮点数的表示方法也和科学技术法类似。通过观察,很容易就发现,因为尾数的整数部分总为1,于是在存储的时候只用存储小数即可。而对于基呢,只要约定好,那完全可以不用占存储空间。这些数据的存储却和整数的存储不一样,用的不是补码。因为用补码的话,会给浮点数的比较造成困难。对于指数,为了方便比较,用的是移码表示,也就是说,在原码上加上一个偏置量,让其变为一个非负数。而非负数的比较完全可以从最高有效位遍历比较,对于硬件设计来说方便快捷。而尾数的小数部分就按一般的二进制小数来存储。因此,对于浮点数来讲,需要存储的就三个部分的数据,尾数的小数部分,符号以及指数。为了区分,现在把指数加上偏执量,也就是实际存储的数据称为阶码。
那么,一个浮点数与机器码的对应形式如下,为了方便,假设基为2:
bias表示的就是移码的偏置量。那么这个偏置量应该是多少呢?
因为exp为k比特的二进制数,所以它最大值为 2k−1 。所以为了它实际表示的指数对称,那么偏置量的取值应该是 2k−12 左右。于是,现在这个问题就变成了,偏置量是要选 2k−1 还是 2k−1−1 的问题。
很明显,偏置量取 2k−1−1 比取 2k−1 ,所能表示的浮点数范围大一倍。所以显而易见的,偏置量应该选取 2k−1−1 。
在很久以前,浮点数很多公司所用的基都不一样。所以为了统一标准,比如基到底是多少比较恰当。IEEE就定了个IEEE754标准来约定,这样方便程序的移植和通用。
IEEE754规定了基为2,然后还规定了三种情况的值:规格化数,非规格化数,特殊数(无穷大和NaN)。
情况 | exp | frac |
---|---|---|
规格化数 | 既不全0也不全1 | \ |
非规格化数 | 全0 | \ |
无穷大 | 全1 | 全0 |
NaN | 全1 | 不等于0 |
又提出了浮点数的几种类型,float,double和long double。 需要注意的是long double在标准里并没有明确的规定,但标准中对于扩展做了规定,所以下表中的Quadruple和Extended都是合法的。像Intel的协处理器在中间过程中用的就是Extended,为了保持精度。
参数 | Single | Double | Quadruple | Extended |
---|---|---|---|---|
阶码的位宽 | 8 | 11 | 15 | 15 |
尾数中的小数部分的位宽 | 23 | 52 | 112 | 64 |
符号位的位宽 | 1 | 1 | 1 | 1 |
总位宽 | 32 | 64 | 128 | 80 |
C语言类型 | float | double | long double | long double |
为了方便,下面都是默认以单精度作为例子。
规格化数是最普遍的形式。它的阶码既不是全零也不是全一。那么这时候。它的最大值的时候就是他的阶码为除了最后一位为0,其他全为1,尾数的小数部分全为1的时候,这时,用十进制表示就是是 (2−2−23)×2127 ,同理可得最小的正数是 2−126 。于是就能在数轴上如下图表示。
可以看见靠近0的地方有个gap,把正数部分的gap附近放大,可以画出大致分布如下图:
很明显,阶码每大一,所占的范围就翻一倍,而尾数所能表示的个数确实不变的。那也就是说上图中,每段区域里的数值间隔都比左边邻近它区域里的数值间隔大一倍,越靠右的区域越大也越稀疏。最小的区域也就是[ 2−126 , 2−125 )里的数值之间的间隔是 2−23×2−126 ,也就说比前面的GAP小 2−23 倍,这就给实际使用带来很多的不变,于是就有了下面的非规格化数。
根据上面的讨论,非规格化数就是为了填gap,实现一下均匀过渡的。很明显,正上溢区,也就是上图中的GAP的长度是 2−126 ,而非规格化的个数就是尾数所能表达的所有值,一个有 223 个,那么若均匀分布,间隔就是 2−23×2−126 。
IEEE委员会也是这么考虑的,规定指数为 1−bias 。因为单精度的bias就是127,那么指数就是 −126 ,这完全和我们刚才的讨论一样。
于是非规格化数和机器码的对应公式如下
当然因为非规格数的指数是固定的,于是也就不用存储指数,因此,机器码里的阶码全为0,这也方便和规格化数分开。
因此,非规格化数的有以下两个作用
给出了一个对零的表示方法,并且正负零的机器码还不一样。因为规格化数的整数部分总是1,所有它并不能表示零。
表示那些非常接近零的数。也就是前面的负下溢区和正下溢区里面的数值。
最后的这类数值有正负无穷大和NaN。他们是在阶码为1的时候表示的。
浮点数的运算,就相当于指数运算。因为非规划数的指数相同,就对尾数进行计算即可。重点在规划数上,现设两规格化浮点数分别为 A=Ma⋅2Ea,B=Mb⋅2Eb ,则:
为了方便,现只针对加法运算进行讨论。
因为数据的存储长度是固定的,那么在加减运算时对阶后,可能会造成很大的精度损失。假设现在进行的是有效位只有三位的加法运算, 1.99+4.56∗102 ,那么首先需要对阶。理应变成 0.0199∗102+4.56∗102=(0.0199+4.56)∗102 ,而因为位宽限制,只能变成 (0.01+4.56)∗102 。这样就会造成很大的精度损失。于是IEEE就规定中间结果必须有两个附加位。它的作用就是左规(结果的整数部分大于1位,这样需要小数点左移,指数变大)和作为最终结果舍入时的参考。
至于舍入,IEEE规定了四种方式:
其实舍入,说白了就是因为有效位的限制,所以紧扣有效位,完全能避开舍入陷阱。
转载请注明:http://djjowfy.com/2017/10/03/深入理解浮点数/