daryl
  • 4.6k

浮点数那些事儿

 阅读约 7 分钟

本文为作者自己的总结的,由于作者的水平限制,难免会有错误,欢迎大家指正,感激不尽。

说起浮点数,大家都是又恨又爱的。爱呢,是因为,只有它可以方便地使用小数;恨呢,是因为它并不能精确地表示小数。

以 PHP 为例:floor((0.1 + 0.7) * 10) 这样一个函数调用,根据数学老师死得晚原理,大家都能得出 8 这个结果。可是事实上呢?它会返回 7。数学老师的棺材板。。。(╯‵□′)╯︵┻━┻

可是为什么会出现这种情况呢?这就要从浮点数的特性说起了。

万物皆二进制

我们都知道,在计算机中,一切的一切都是二进制表示的。假设一个 4 字节整型的十进制数 8,在大端表示的机器中,表示成 00000000 00000000 00000000 000010000x0000008)。将十进制整数转换成二进制数,是非常容易的。可是,小数呢?比如,我们要表示 1.75,该怎么存储在计算机中呢?显然,不能像整数一样存储了。

小数的二进制

让我们回忆一下,在十进制中,小数是怎么计算的。上面的 1.75 我们是这么算的:1 × 10^0 + 7 × 10^-1 + 5 × 10^-2 。那么我们按照相同的规则,来用二进制计算一下小数部分:0.75 = 1/2 + 1/4,也就是 1 × 2^-1 + 1 × 2^-2 ,再加上前面的整数部分,那么整个式子就变成了 1 × 2^0 + 1 × 2^-1 + 1 × 2^-2 ,写成二进制形式就是 1.11。所以,1.75 的二进制表示是 1.11。

对于将小数转换为二进制,和整数部分除二取余相反的,是乘二取整。

0.75 * 2 = 1.5 -> 1
0.5 * 2 = 1 -> 1

所以我们同样可以得出 1.11。

科学计数法

好了,我们已经知道如何表示一个小数的二进制了。辣么,问题来了。学过 C 语言的同学都知道,一个 float 只有 4 字节,一个 double 也只有 8 字节。那么,这么表示一个小数,好像范围很有限。

在数学老师哭晕在厕所之前,我们应该还记得十进制数中有这么一个东西——科学计数法,我们可以很方便地用它来表示很大的十进制数。那么,同理,我们也可以用在浮点数的表示上。

让我们先来回忆一下,科学计数法的表示。假设我们有一个数 17500,我们可以用科学计数法表示成 1.75 × 10^4 。我们照葫芦画瓢,在二进制数中,假设有一个数是 11010。我们来和十进制对应一下。十进制是乘 10,那么二进制就是乘 2,我们对应的就可以写成 1.101 × 2^100 。对,其实就是这么简单。那也许有的人会问了,为什么不写成 0.1101 × 2^101 呢?我们再来回忆一下,在十进制科学计数法中,是不是有一个规定,整数部分的范围是 [1,10)。那对应到我们的二进制数上,这个规定就可以变成 [1,2) 了,没错,对应关系就是这么简单。

浮点数

好了,我们现在也知道怎么使用二进制来表示小数,以及使用科学计数法来表示二进制小数了。那么,我们距离把数字存入计算机内存仅剩一步之遥了,我们要把所有的东西存到内存里去,那么我们就需要合理地分配内存空间。浮点数有两种,一种是单精度浮点数(float),占用 4 字节的内存。其中,1 位是符号位,8 位是阶码(幂),23 位是尾数(小数部分)。

细心的各位可能会发现,好像没有整数部分?别急,这就是上面那个规定的有用之处。当整数部分在 [1,2) 之间时,也就只可能取到一个值 1,那么,对于这个值,我们是不是就可以当做默认值而不记录在浮点数的表示中了?而这样,我们的浮点数的精度又多了一位(小数部分的位数决定了精度)。这种表示叫做隐含 1 开头的表示。

规格化与非规格化

偏置值

到了这里,我们发现,第一位是浮点数的正负符号,那么,对于一个科学计数法来说,阶码同样需要有正负。而在单精度中,阶码只有 8 位;双精度中,阶码只有 11 位。如果我们给阶码表示成补码,那么,我们能够表示的数的范围就会缩小,这样显然是不划算的。于是,偏置值就由此诞生了。

规格化的值(阶码不全为 0 或 1)

在内存中的规格化的浮点数表示中,阶码并非是 2 的幂,而是经过计算的结果,这个计算公式就是 e - Bias,这里的 Bias 就是偏置值,而 e 就是阶码在浮点数中的二进制表示。Bias 的值是 2^k-1 - 1(单精度是 127,双精度是 1023),所以,e - Bias 的取值范围就是 [-126, 127](单精度)和 [-1022, 1023](双精度)。其实如果对补码了解的比较好的同学,应该就能看出来,这其实就是省略了符号位的补码表示)。

通过上面的隐含 1 开头的表示的尾数,我们可以计算出基数 M = 1 + f。那么我们整个的浮点数可以写成这样一个表达式:M × (e - Bias)。

非规格化的值(阶码全为 0)

对于规格化和非规格化的值来说,我们都可以用同一个式子来表示。不过,为了某些更加方便的原因(这里就不展开讲了),对它们做了区分。如果按照规格化的计算来看,阶码的值是 0 - Bias,不过在这里,我们让阶码的值等于 1 - Bias。同样的,由于我们给阶码加了 1,那么整个浮点数就会向左移动一位,那么,我们需要让浮点数的值不变,M 就不在需要上面整数部分的 1 了,所以 M = f

同时,我们会发现一个问题,那就是 +0.0 和 -0.0 在浮点数的二进制表示上是不同的。

特殊值(阶码全为 1)

最后,还剩下这样一种数字,那就是阶码全为 1 的情况。当小数为 0 的时候,浮点数的值为 ∞。当小数不为 0 时,浮点数的值为 NaN,即不是一个数(Not a Number)。

计算浮点数

好了,扯了这么多,我们现在回到最开始的问题上,floor((0.1 + 0.7) * 10) = 7。我们先看 0.1 的二进制表示。

首先,我们将十进制小数转换成二进制小数,可以得到 0.000[1100]···。让我们转换成浮点数的二进制表示。按照上面的规则,它可以被表示成科学计数法 1.10011001100110011001100 × 2^-4 ,这样,阶码就是 -4 + 127 = 123,二进制表示为 01111011。所以,整个浮点数的二进制表示就是 00111101110011001100110011001100(0x3dcccccc)。同样的,0.7 会表示为00111101001100110011001100110011(0x3d333333)。

首先我们要对阶码小的数进行对阶,然后再进行尾数的加法,这样,我们得到的值就是 00111101111001100110011001100101。我们将其转换成十进制,发现,它是小于 0.8 的。因此,当我们再进行乘法运算向下取整时,会等于 7。

最后

其实,浮点数有很多坑。因此,我们在使用浮点数的时候,一定要小心。还有,涉及到金额计算的时候,一定不能使用浮点数。

本文为作者自己读书总结的文章,由于作者的水平限制,难免会有错误,欢迎大家指正,感激不尽。

参考文献

《深入理解计算机系统(第 3 版)》第 2.4.2 节

阅读 2k发布于 2017-09-24
推荐阅读
daryl的技术天地
用户专栏

40 人关注
23 篇文章
专栏主页
目录