手记

浮点数杂想

距上一次写原码,反码,补码杂谈已经有一段日子了。关于浮点数,我也有一些自己的思考,想写出来,但是日月如梭,人懒如dog呀。所幸有一个周末,在一家咖啡厅里,喝了杯柠檬滇红(咖啡店喝红茶,好吧这不是重点),我就开始动笔了。

IEEE754 浮点表示法


根据IEEE754 表示法,一个数可以表示为 :

V= (-1)^SM2^E

  • 符号 s(sign)
    s决定这个数是负数还是正数
  • 尾数 M
    M是一个二进制小数
  • 阶码 E(exponent)
    E 的作用是对浮点进行加权,这个权重加权,这个权重是2的E次幂(可能是负数)

好了,从《深入理解计算机系统》中把这段抄了下来,本文终..........

引言


浮点数 (floating point number) ,顾名思义,小数点(point)浮动表示。

从上面我们可以看出浮点数的表示法,比原来我们表示整数的时候直接把一个十进制数表示为二进制数,看上去复杂多了。

那么,我们为什么需要浮点数表示法呢?

  • 为了存储小数?
    直接用二进制表示法,也可以表示小数。十进制小数表示为浮点数表示法的常用计算方式,也是先将小数转为二进制表示,然后再进行移码。
  • 因为小数无法精确存储?
    用浮点数表示 可以精确存储了吗? 并没有。

我个人的 观点是

使用浮点数表示法 是为了解决 在有限位数里,能表示的小数太少 。并非 为了解决 小数在计算机中无法精确存储。

其实浮点数表示法 并不复杂。我觉得它的想法和高中数学中的一个函数有一种千丝万缕的关系。

从二进制表示法说起


计算机的存储方式是通过二进制存储的。如果按照整数的存储方式,我们要表示小数也同样要先将十进制的小数转化为二进制表示法。
例如:

二进制 分数 十进制
0.0 0/2 0.0
0.1 1/2 0.5
0.01 1/4 0.25
0.001 1/8 0.125
0.0001 1/16 0.0625
0.00001 1/32 0.03125

结果我们发现其实大部分的小数,因为无法被2整除,而无法用二进制精确表示。

其实从严格意义来说,二进制表示法也不能表示所有的整数,因为位数有限。那么我们如何 表示更大的数呢?扩增位数。

那么对于二进制小数,我们也可以借鉴这种想法,扩增位数,表示更多的小数,对于某些无法精确的小数总是可以在有限范围内通过舍入来表示。

定点表示法的困境


可实际上,我们如果把扩展位数x和所能带来的表示范围y,画成图像。
从图像上看 其实 就有点像高中的幂指数函数 :


(但是x的取值是离散的,只能取整数,y的取值也是离散的。所以图像不是连续的。但是找个图不容易,暂且将就着看吧。)

从图中来看 :

  • 整数
    对于整数来说,他对应的是2^x函数的第一象限。在该象限里,随着x也就是位数增加,可表示的范围y呈指数型增长。

  • 小数
    但是对于小数来说,对应的是2^x第二象限。在该象限里,随着位数的增加,可表示的范围y增长趋于平缓。也就是说位数扩增对于可表示的浮点数精度,效果并不明显。
  • 结论
    对应的大白话来说就是:可能对于底层硬件扩增了n位,但是所能表示的十进制数可能最小精度值一位都没移动。
    典型地,你看:
二进制 分数 十进制
0.0001 1/16 0.0625
0.00001 1/32 0.03125

二进制扩增了一位,但是对应的十进制精度从0.0625-> 0.03125实际上最小位上的权还是10^(-2) 并没有移动。

这就是定点数二进制表示法在小数部分应用遇到的困境。怎么办?

  • think
    如果我们能把函数图像往 右边移动一下。让小数的表示能落在第一象限。那么对于同等位数的机器,增大 X 所能表示的小数范围 Y 就会相对更大一些,也就是最小精度能更精细一些。
    其实 想到这里的话,我就觉得用上浮点数表示法就很自然了,并不是刻意的。很水到渠成。向右移动这个2^x 函数, 得到的函数图像对应的就是2^(x-E) 函数。其中E这个偏移值 就对应了浮点数中,小数点移动的位数,也对应了浮点数表示法中的阶码(exponent)。
    其实这也有点 科学计数法的感觉。科学计数法是把n个0,写成m * 10^n这种形式,而浮点数表示为 M * 2^E

32位浮点数的二进制表示法


那么根据IEEI754标准的,32位浮点数是如何表示的呢?这里我借用了阮一峰老师画的图。他写的浮点数介绍的文章讲得很简洁明了,一目了然。特意推荐一下:浮点数的二进制表示

由上图:

  • sign
    符号位,用来区分正负
  • exponent
    这个其实应该写作exp才对,真正的exponent 应该指得是 E 阶码。
    并且 E = e-bias 或 E = 1-bias(当exp全为0 时)。其中bias定义为等于(2^k-1)的值,也叫做偏置值。
  • frac
    就是尾数,也叫位域了。
    exp!=0 && exp!=255 M = 1+f
    exp=0时 , M=f

有点绕,直接上实锤吧。
根据

V= (-1)^SM2^E

上图的二进制数表示的浮点数为:

f : 0.01(2) = 0.25(10)
e : 01111100(2) = 124(10)
V = (-1)^S*M*2^E
= (-1)^0 * (1+ 0.25)  * 2^(124-127)
=  1* 1.25*2^(-3)
= 1 * 1.25 * 0.125
= 0.15625

再举个栗子:3.0625的浮点数二进制表示法是

第一步:
3.0625(10) = 11.0001(2)
第二步 : 小数点移位
11.0001 = 1.10001(2)* 2^(-1)(10)
第三步 : 根据(-1)^S*M*2^E  写出s ,exp ,f
s = 0 
f = 10001
exp = E+bias = -1+127 = 126 = 01111110
则3.0625 的浮点表示法为
0 01111110 10001 000000 000000 000000

我们可以看到s和frace 总是比较容易理解的,就是E = e - bias 或 E = 1-bias这个会比较难以理解。
这个我个人的看法是 :

用浮点数表示的意义在于,能在有限的位数里表示更多的数。

E的值如果只是负数的话,或只是正数的话。就只能表示小数或只能表示整数。阶码的正负值使浮点数的表示能同时覆盖到整数和小数。
那么,为什么E 的表达方式不和我们补码的那种模运算表示法一样呢?这个我也还没想明白。

但是,对于这个疑问?《深入理解计算机系统》第二章里的介绍关于 规格化数 和 非规格化数这几个概念时 有提过这样设计的原因 :
(1) 是为了将非规划化数和平滑过渡到规格化数
(2) 是为了浮点数能使用整数排序函数来进行排序

  • 舍入
    我们 还可以看到32位的浮点数,frace的位数是23,那么如果一个小数表示为二进制数,一直除不尽,浮点移位后 frace > 23呢?怎么办?
    舍入。根据IEEE754的标准是向最接近的能精确表示的小数舍入,如果最精确的有两个,就向最低位是偶数舍入。

    这样可以保证50%的情况是向上舍入,50%是向下舍入,不会在计算这些数的平均值时带入偏差。

好吧,还是《深入理解计算机系统》第二章说的,它有详细地介绍。但是我已经放弃了。哈哈哈.......

浮点数不能直接比较相等


由于浮点数的非精确存储,所以两个浮点数不能直接比较相等。

未必。这里,请允许我插入一张图-----轮子哥在知乎的回答。

由于浮点数的非精确存储,带来的问题通常是在计算时引起的。并且实际上计算的顺序也会影响丢失的精度范围。而对于未经过计算的,总不可能上一行代码中的二进制表示法和下一行的就不一致了。他就算不精确存储也应该有同一种相同的舍入策略(指存储时的舍入策略,而非计算时的截断策略)。所以,对于未经过计算的数值大可放心地直接进行数值比较。
常见场景 : 用户输入一个小数,判断是否小于等于一个常量(小数)。这种可以放心地使用 <=

那么如果对于经过计算的浮点数,要如何比较是否相等呢?
上一段熟悉的代码,大一在机房看到比较两个数是否相等要取绝对值 < 0.00001 一脸懵逼.gif 影响深刻。

    #define EPSILON 0.000001    //根据精度需要
    if ( fabs( fa - fb) < EPSILON )
    {
            printf("fa<fb\n");
    }

为何要取0.00001? 好吧,我也在找这个答案。

浮点数与PHP


  • 在PHP中如何比较浮点数
    关于浮点数的比较PHP手册也有介绍

    在PHP中,如果需要比较浮点数的大小,可以使用bccomp()函数进行比较。
    bccomp()其实是BCMath提供的一个函数。如果你需要更精确地进行浮点数的运算,都在BCMath中可以找到对应的函数。

值得一提的还有这段:

 2^ (-16) = 0.0000152587890625   => 接近于精度  0.00001

这会不会就是浮点数比较大小的epsilon 要取0.00001的由来?

  • PHP中没有整型溢出,浮点数也不会溢出。
    与传统的强类型语言不同,php中并不会出现整型溢出现象。当一个int型的数溢出时会自动转为float类型,当一个float类型的数溢出时会维持float所能表达的最大值。

第一次发现这个是因为大三学单片机,调C代码的下溢bug。然后就很好奇在php中如果int类型的数是否有溢出的问题。用上面的代码测了一下,发现结果并没有,这个具体是怎么做到的,我也很好奇。就先把坑挖在这里留待以后解决吧。

以上就是我对浮点数的 一些思考。由于作者见识有限,文中难免纰漏繁多。欢迎读者交流指正。


参考阅读:

5人推荐
随时随地看视频
慕课网APP

热门评论

图片看不见了

查看全部评论