oynix

于无声处听惊雷,于无色处见繁花

摸摸「浮点数」的底

摆理论和套公式其实也是为了说明问题,解释原理,不过有时却适得其反。

前一个主题介绍了整数在计算机里的存储形式,浮点数,即小数,例如 3.14、2.71828 等,看起来和整数相近,而二者在计算机中存储的形式大为不同,作为一种常用的数据类型,来了解了解其中的本质,这个主题我们一起来摸摸「浮点数」的底。

浮点数有多种类型,但是我们写代码时常用到的有两种,按照所占长度分为:一个是单精度,占用 32 个二进制位,另一个是双精度,占用 64 个二进制位。不管是什么类型、占用多少位,存储的模式都是类似的,而这也正是我们想要摸的「底」。

1. 化身为二进制的浮点数

首先明确一点,不管什么类型的数据,整数也好,浮点数也罢,在计算机中都是以二进制形式存储,即 01001、10110 这样的形式。

那么,二进制的浮点数该如何表示呢?

在此之前,我们先来看下十进制的小数 22.71是如何表示的:小数点左边第一位为个位,表示 1,第二位为十位,表示 10;小数点右边第一位为十分位,表示 1/10,即 10^-1,第二位为百分位,表示 1/100,即 10^-2,综合如下

1
2
3
22.71 = 2*10^1 + 2*10^0 + 7*10^-1 + 1*10^-2
= 20 + 2 + 0.7 + 0.01
= 22.71

上面就是十进制浮点数的表示形式,而二进制与十进制整体相同,唯一区别就在于将 10 换成了 2,举个例子,如二进制浮点数 101.11:小数点左边第一位表示 2^0,即 1,第二位表示 2^1,即 2,第三位表示2^2,即 4;小数点右边第一位表示 2^-1,即 0.5,第二位表示 2^-2,即 0.25,综合如下

1
2
3
101.11 = 1*2^2 + 0*2^1 + 1*2^0 + 1*2^-1 + 1*2-2
= 4 + 0 + 1 + 0.5 + 0.25
= 5.75
2. 自由转化的浮点数

既然,浮点数既可以用十进制表示,也可以用二进制表示,那么同一个浮点数如何在这两种进制之间自由转化呢?由二进制转为十进制就不用说了,上面刚刚用 101.11 演示完,而由十进制转为二进制很多人都是直接套公式却没想过原理,接下来说说推导公式的过程。这个转化过程需要将浮点数的整数部分和小数分别转化,之后再进行合并

我们用 6.625 来举例说明。先看整数部分的 6,这个我们一眼就能看出来,结果是 110,但是如果换成 60、600,这些不能一眼看出的数呢?其实很多事情都是如此,将思考过程从「一眼就能看出的」的情况中抽象出来,就是计算方法。这个思考过程无非就是,想知道需要几个二进制位、每个二进制的值是多少,就可以表示 6。

为了便于理解,我们先看一下计算表示 758 需要几个十进制位以及每位的值是多少。

1
2
3
4
5
6
7
8
9
10
11
12
13
先从个位,即右起第一位开始:

很显然,求个位的值用 75810 取余即可:758 / 10 = 75···8,结果为 8,此时原数还剩下 75*10,大于 0,说明一位不够;

继续求第二位的值:75*10 / 100 = 75 / 10 = 7···5,结果为 5,此时原数还剩下 7*100,大于 0,说明两位还是不够;

继续求第三位的值:7*100 / 1000 = 7 / 10 = 0···7,结果为 7,此时原数还剩下 0*1000,等于 0,说明三位十进制够用了。

最终结果,需要 3 个十进制位,从左到右的值分别是 758,即 758

同时我们发现的一个规律是:
求第二位的值时,用计算完第一位的商对 10 取余即可,
求第三位的值时,用计算完第二位的商对 10 取余即可。

由此可得,求第 n 位的值时,用计算完 n-1 位的商对 10 取余数即可,当商为 0 时,结束,注意 n > 1。如上,就是十进制抽取方法的过程,也同样适用于二进制,区别就在于将 10 换成了 2,实际计算一下。

1
2
3
4
5
6
7
8
9
计算将 6 用二进制的值,同样从右起的第一位开始:

先求第一位:6 / 2 = 3···0,结果为 0,原数还剩下 3*2^1,大于 0,说明一位不够;

继续求第二位,3 / 2 = 1···1,结果为 1,原数还剩下 1*2^2,大于 0,说明两位不够;

继续求第三位,1 / 2 = 0···1,结果为 1,原数还剩下 0*2^3,等于 0,说明三位二进制够了。

最终结果,需要 3 个二进制位,从左到右的值分别是 110,即 110,而这也与我们直观上看到的结果相符。

与整数部分的计算类似,再看下小数部分的计算。同样,先用比较直观的十进制浮点数举例,从而从中总结规律。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
计算 0.358 用几位十进制的浮点数表示,及每位的值的过程,从左开始,

先求第一位,注意,与整数不同的是,这里是取商,而不是余数:
0.358 / 10^-1 = 0.358 * 10 = 3···0.58,结果是:3,原数还剩下:0.58*10^-1 > 0;
继续求第二位,
0.58*10^-1 / 10^-2 = 0.58 * 10 = 5···0.8,结果是:5,还剩下:0.8*10^-2 > 0;
继续求第三位,
0.8*10^-2 / 10^-3 = 0.8 * 10 = 8···0,结果是:8,还剩下:0,说明 3 位十进制够了。

最终结果,需要 3 个十进制,从右向左分别是 358,即 0.358

同时,我们发现:
求第二位时,用计算完第一位的余数乘 10 再取商即可,
求第三位时,用计算完第二位的余数乘 10 再取商即可。

由此可得,计算第 n 位时,用计算完第 n-1 位的余数乘 10 再取商即可,当余数等于 0 时结束,注意 n > 1。紧接着,用我们从上面十进制例子中总结的方法计算下二进制。

1
2
3
4
5
6
7
计算 6.625 的小数部分 0.625 的二进制形式,从左开始,

先求第一位,0.625 * 2 = 1···0.25,结果是 1,原数还剩下:0.25*2^-1 > 0
继续求第二位,0.25 * 2 = 0···0.5,结果是 0,原数还剩下:0.5*2^-2 > 0
继续求第三位,0.5 * 2 = 1···0,结果是 1,原数还剩下:0,结束。

最终结果,需要 3 个二进制位,从左向右分别是:101,即 0.101

至此,我们完成了所有计算过程,整数部分:110,小数部分:101,最终结果为:110.101。

3. 如何存储

前面做了那么多铺垫,现在终于可以开始说存储了。存储之前,需要先把二进制的浮点数用科学计数法表示,也称规范化,即

N = ±a x 2^n , a∈ [1,2)

a 叫作尾数,n 叫作阶码。计算机存储的浮点数都是这种形式的,例如上面的 110.101 就需要变成 1.10101x2^2,而 0.1101 就需要变成 1.101x2^-1,这个变换的过程很简单。

对于浮点数 ±a*2^n,计算机将其分成了 3 个部分来进行存储,即存储正负的符号位、存储 n 的指数域和存储 a 的尾数域,是的,存储 a 的部分叫做尾数域

对于长度为 32 位二进制的浮点数,从左起,第 1 位用来表示符号,0 代表正数,1 代表负数。接下来的 8 位,即第 2 位到第 9 位,用来表示指数。而从第 10 位开始一直到最后的 32 位,都用来表示尾数,对于科学计数法下的 a,其整数部分的值永远都是 1,所以这个 1 就省略了,因此 23 位尾数二进制只存储了 a 的小数部分,即 1.10101 的 10101,1.101 的 101。

和 32 位类似,长度为 64 位二进制的浮点数,从左起,第一位存储符号,接下来的 11 位存储指数,剩下的 52 位存储尾数,而尾数同样省略了 a 的整数部分,只存储小数部分。

4. 阶码

符号位,0 或 1,尾数域,采用原码表示,这两种比较简单明了,这里要单独说一说阶码。

在此之前,先回忆一下移码。所谓移码,实际上就是把负数映射到正轴上,通俗的说就是消灭负数。例如,4 个bit位能表示的范围,[-2^3, 2^3 - 1],即[-8, 7]。每个数字加上偏移量 2^3,即为移码。

阶码在计算机中也是以移码的形式存储的,区别在于这个偏移量。根据 IEEE 754 标准,k 位二进制位的解码,偏移量 bias 为 2^k-1 - 1,例如 32 位单精度浮点数,阶码为 8 位,则偏移量为 2^8-1 - 1 = 127。

N = (-1)^S x 2^E-Bias x (1+M)

其中,S为符号位的值,E为指数的值,M为尾数域的值,Bias为移码偏移量

举个例子,-6.75,单精度浮点数,转化为规范化二进制为:-110.11 = -1.1011 x 2^2

符号位 S = 1

阶码 E = 2 + Bias = 2 + 127 = 129

尾数 M = 1.1011 - 1 = 1011 (省去小数点)

1 10000001 10110000000000000000000 (尾数域:4 位精度 + 19 个 0,共 23 位)

至此,浮点数就介绍完了。

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2019/06/96f371e1637d/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道