仓库源文站点原文


title: 关于浮点数的剪不断理还乱 layout: post categories: 编程 tags: 浮点 定点 二进制小数 规约 阶码

excerpt: 展开介绍关于浮点的表示与转换原理

二进制小数

谈浮点数前,先了解一些基础知识;对于整数的十进制与二进制转换不难,原理也简单就不再赘述,假如现在要进行转换的十进制数字是带有小数点的,转换方法就会稍微有点不一样了;为了使说明更浅显易懂,先来探究一下十进制小数中的奥秘;

以十进制数 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 而已;

定点数

当然,都知道机器码都是一堆 01 组成的数据,并没有小数点这个专门的符号,要表示数字 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 位的值);

NaN

如果 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 位系统也是稀有物种了吧;因此,浮点数有效的扩大了能表示的数据范围,科学计数法减少了书写量,浮点表示则是节省了存储空间;

另外也是由于科学计数本身的特性,以及指数偏移值,也就是 阶码 的应用,小数点也就不再像之前一样固定,具体位置会根据指数的大小最终“漂浮”到不同的位置,甚至到那遥远的地方……