title: 关于浮点数的剪不断理还乱 layout: post categories: 编程 tags: 浮点 定点 二进制小数 规约 阶码
谈浮点数前,先了解一些基础知识;对于整数的十进制与二进制转换不难,原理也简单就不再赘述,假如现在要进行转换的十进制数字是带有小数点的,转换方法就会稍微有点不一样了;为了使说明更浅显易懂,先来探究一下十进制小数中的奥秘;
以十进制数 3.125 为例,小数点以左为整数部分,右为小数部分,它也可以被更具体的拆解为以下形式:
3x10^0 + 1x10^-1 + 2x10^-2 + 5x10^-3
这也是十进制数的特点,基数为 10,所以每一位都是与 10 的相应次方的乘积,指数也是有规律的,可以看成以小数点为对称轴,向左就是 10 的 0、1、2、3……次方递增,向右则是 10 的 -1、-2、-3……次方递减,如果换一种形式,那么在运算方面也具有对称性(乘法与除法互为逆运算),就是下面的形式:
3x10^0 + 1/10 + 2/10^2 + 5/10^3
这样,理解二进制的小数部分就容易了,已经知道整数十进制转二进制其实就是连续除以 2 取余数,那么根据上面的规律,小数部分看出逆运算,就可以总结为 连续乘 2 取整数,举例说明:
3.125(十进制)
3 / 2 = 1 --> 1
余 1 --> 1
0.125 x 2 = 0.25 --> 0
0.25 x 2 = 0.5 --> 0
0.5 x 2 = 1 --> 1
==> 11.001(二进制)
11.001 就是 3.125 的二进制小数形式,同时它也可以做如下拆解:
1x2^1 + 1x2^0 + 0x2^-1 + 0x2^-2 + 0x2^-3
= 2 + 1 + 0 + 0 + 0.125
= 3.125
可以看到也是与十进制形式相对应的,只是基数从 10 换成了 2 而已;
当然,都知道机器码都是一堆 0
和 1
组成的数据,并没有小数点这个专门的符号,要表示数字 3125 还好说,但是 3.125 中间多出的这一点要怎么解决呢……一个很直接的方法就是把小数点应该在的位置焊死 -_-,比如 32 位的系统,规定小数点在 16 位和 17 位之间,那么根据 3.125 的二进制是 11.001,在 32 中的表现就是:
0000 0000 0000 0011 0010 0000 0000 0000
这就是 定点数 的规则干的事情,看着挺好,要是多用几次系统空间可能就 hold 不住了,不好之处明显就是花掉当前一半的数量级取表述小数部分,当然可以少划分几位给小数,但对小数好像不太公平,万一精度要求高呢,反过来呢对整数又不公平了,手心手背都是肉,得想办法解决才行;
于是乎,浮点数横空出世;其实上述问题平时肯定都遇到过,比如记个帐,昨日净收入 31200000000
元(@_@),肯定不想写这一堆 0 对不对(如果是真的咱愿意写 100 遍……),一般都会写 312 亿或者 3.12x10^10
元,重点就在后面这种科学计数表示法,人为了省力省纸省笔墨弄了个科学计数法,处理器也自然用上了浮点数,省空间;具体原理与表示方法后面会讲;
以 C语言为代表,其中就有浮点这一数据类型,比如单精度浮点 float(32 位),双精度浮点 double(64 位)等,可能平时也就直接声明和使用较多,没太关注底层实现的算法,其实也不太复杂,套用一种规则而已,下面开始介绍;
目前关于浮点运算与表示,使用广泛的应该就是 IEEE 754(二进制浮点数算术标准)了,其主要内容如下:
v = (-1)^s * 2^e * m
v: 浮点数具体值
s: 符号位,即正负号,0 为正,1 为负
m: 有效数,也叫尾数,可以类比科学计数法前面的有效数字
另外还有一个小数位 f, m = 1 + f
e: 指数位,即 2 的多少次方
该标准包含了多中位数,以 32 位为例:
(1) (8) (23) : 位数
0 00000011 0010000000000000000000
s e f
总结就是:
64 位的规则是:
知道了这些还不能立刻套用上面的公式经行转换,还需要了解接下来的一些规定;
浮点的二进制表示中指数位 e 的计算值(即转换成十进制后的值),要在实际指数值(十进制)的基础上加上一个 偏移值,标准中规定偏移值为 2^(e-1) - 1
,如 32 为中 e 是 8 位,所以偏移值为 127,64 位的就为 1023;所以 32 位指数实际值的范围为 -126 ~ 127
;
举个例,指数 e 的实际值为 -3,那么 32 位中加上偏移值就是 -3 + 127 = 124
,换算成二进制的 8 位指数位就是 01111100
;
这种指数位偏移后的指数值,又叫做 阶码,因为科学计数法中的指数是有正负之分的,所以实际指数值加上一个正的适中偏移值,就可以使得浮点表示法中的指数位为无符号的整型(就是变成正整数),利于浮点数的比较大小,就是可以直接从浮点的二进制表示中,由高位向低位逐位进行比较(如果是负数二进制比较大小要复杂一点)。
具体的表示方式会根据不同的情况而不同,主要有以下几种情况;
即 e 的 8 位数字不是全部为 0 或者 1,此时 m = 1 + f
,由于小数部分 f 的值在 0 到 1 之间,所以有效数 m 的值在 1 到 2 之间;
这种形式 e 的 8 位全部为 0,小数位 f 值不为 0,用于表示非常接近 0 的数,此时不再是 m = 1 + f
,而是 m = f
,即 m 值在 0 到 1 之间;实际上所有非规约的浮点数比规约浮点数更接近于 0;
指数位 e 全为 0 的同时,小数部分(f)为 0,用来表示 ±0
(正负取决于 s 位的值);且规定最小指数位编码(e = 0 时)的实际值应该取 -126
(本来应该是 0 - 127 = -127
);
如果 e 全为 1,且 m 全为 0,则表示无穷大(Infinity,正负取决于 s 位的值);
如果 e 全为 1,且 m 不全为 0,则表示 NaN
(Not a Number,非数值类型);
后面的例子都以 32 位为例,其它位数根据标准类推;
先来看一个十进制转浮点,规约形式的例子,比如用之前的十进制数 3.125,转换为 32 位浮点二进制格式(0b
开头的表示二进制数据):
3.125
= 0b11.001
= 0b1.1001 x 2^1
s = 0
e = 1
=> 1 + 127
= 128
= 0b10000000
m = ob1.1001
f = m - 1
= 0b0.1001
=> 0b10010000000000000000000
==>
0 10000000 10010000000000000000000
转换的大致流程总结如下:
十进制小数 --> 二进制小数 --> 浮点表示法 --> 二进制浮点
再举一个二进制浮点转十进制小数例子:
1 01111111 00100000000000000000000
s = 1
=> -1
e = 0b01111111
= 127
=> 127 - 127
= 0
f = 0b0.001
m = 1 + f
= 0b1.001
v = -1 x 0b1.001 x 2^0
= 0b-1.001
= -1.125
==> -1.125
以 32 位单精度浮点数为例,由于分配给有效数字的位数是 23 位,而整数部分默认是 1,它的位置就不用留了,所以小数部分就可以独占 23 位,在加上默认的一个整数位就是 24 位了,同理,64 位双精度浮点数的有效数就是 53 位(52+1),再进行一下算术运算:
log(2^24) = 7.22
log(2^53) = 15.95
上面的算式表示二进制下的这么多位数的实际值,对应到十进制中有多少位;结果表明,单精度浮点可以保证 7 位十进制的有效数字,双精度的则可以保证 15 位;
取名为浮点,那么到底“浮”在了什么地方,与定点数相比的优势又是什么?总的看来,其实其转换操作很类似于十进制中的科学计数法,而科学计数法的出现,就是为了实现能简短地书写较大的数,比如写作 1.0x10^20
,就可以避免在 1 后面写那令人抓狂的 20 个 0 了;
同理,二进制中如果用定点数表示小数,那么 32 位的话就最多到 32 位有效数字,而用这种类似科学技术的浮点表示法的话,指数能表示到 100 以上,也就是 100 多位了,相信现在的 100 位系统也是稀有物种了吧;因此,浮点数有效的扩大了能表示的数据范围,科学计数法减少了书写量,浮点表示则是节省了存储空间;
另外也是由于科学计数本身的特性,以及指数偏移值,也就是 阶码 的应用,小数点也就不再像之前一样固定,具体位置会根据指数的大小最终“漂浮”到不同的位置,甚至到那遥远的地方……