距上一次写原码,反码,补码杂谈已经有一段日子了。关于浮点数,我也有一些自己的思考,想写出来,但是日月如梭,人懒如dog呀。所幸有一个周末,白无聊懒,我就开始动笔了。
IEEE754 浮点表示法
根据IEEE754 表示法,一个数可以表示为 :
- 符号 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
有点绕,直接上实锤吧。
根据
上图的二进制数表示的浮点数为:
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类型的数是否有溢出的问题。用上面的代码测了一下,发现结果并没有,这个具体是怎么做到的,我也很好奇。就先把坑挖在这里留待以后解决吧。
2018:10:01 新增加
php中int类型和float类型溢出时,自动转化这个问题。鸟哥的博客有介绍。
详见 : 关于PHP浮点数你应该知道的(All 'bogus' about the float in PHP)
以上就是我对浮点数的 一些思考。由于作者见识有限,文中难免纰漏繁多。欢迎读者交流指正。
参考阅读: