4

关于浮点数的剪不断理还乱

 2 years ago
source link: https://knightyun.github.io/2020/03/21/programme-float-point
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

二进制小数

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

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

总结就是:

  • 第一位为符号位 s;
  • 后 8 位为指数位 e;
  • 最后 23 位为小数位 f;

64 位的规则是:

  • 第一位是符号位 s;
  • 后 11 位是指数 e;
  • 最后 52 位是有效数字 m;

知道了这些还不能立刻套用上面的公式经行转换,还需要了解接下来的一些规定;

指数偏移值

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK