数值的表示
先上代码
1 | // JavaSctipt |
如果你百度一下,就会知道几乎所有语言都会面临浮点数的精度问题,想要了解IEEE754,要先了解计算机内部的定点数表示法。
定点数
以八位机器数举例
原码
1 | 10 => 0000 1010 |
原码就是把数值直接用二进制表示出来,正负号用末位0,1来表示。
原码有诸多弊端,比如:数值的减法不能用加法来计算。在十进制中10-10和10+(-10)是等效的,但是在原码表示法的机器数运算中,10+(-10)却得到了1001 0100的结果,显然不等于0 。在计算机内部,处理运算是用与或非门来控制的,加减法分别用两套电路有些浪费资源,我们能否用加法电路来实现减法呢?
其次,0和-0在原码中竟然是不同的表示法,但是这对运算并没有帮助,反而浪费了一个表示位置。
补码
1 | 10 => 0000 1010 |
补码的正数和原码一样,但是负数采用原码的“取反加一”,也就是说,想要得到-10,首先把符号位确定为1,然后写出10的原码表示,将其各个位置取反加一,即得到了该原码的补码。补码的求相反数同理。
此时10+(-10)和10-10的操作统一了,同时也没有了+0和-0的区别。计算机内部运算一般采用补码来进行,感兴趣的可以了解一下定点数/浮点数的乘法/除法,仅用移位和加法运算即可完成。
补码的移位要注意一点,算术右移的时候正数补0负数补1 。
移码、反码此处不涉及,了解即可。
浮点数
为了扩大数的表示范围,可以采用“科学计数法”来表示大数或者小数。比如-2000 0000就可以表示成1
-1 * 2 * 10 ^ 7
如果我们提前约定好了底数10,那么我们表示这个数只需要三个信息,负号、2、7 ,转换成2进制就是1 10 0111比我们单独存储-20000000要节省很多空间
由于2进制的限制,计算机内部这个约定好的底数是2。
IEEE754
目前计算机内部表示浮点数的一个标准就是IEEE754,以32位计算机举例,表示1100 0111为1 1000 0101 10001110000000000000000
乍一看可能觉得这种表示法更复杂了,其实无论在精度还是在表示数的范围,IEEE754都是很优秀的。
在IEEE754中,底数为2,用1位表示符号,8位表示阶码,也就是上文中的1000 0101,为了表示2的负数次方,阶码采用移码,都加上127,这样负数的阶码也可以表示出来了,23位原码表示尾数。
关于尾数这里要注意一点,由于2进制的有效数字第一位一定是1,所以尾数的前面省略了一个1不写,用23位的尾数表示了24位的值,在手动转换的时候要自己补上。
说了这么多还是没有直接解答浮点数的精度问题,但是看过了以上之后,你会更容易理解。
首先计算机没有办法精确地表示0.1和0.2等等,因为通过2次幂来控制移位,计算机只能精确表示出0.5、0.25、0.125、0.0625等等,这些数是无论如何也不能表示出0.1这样的数的,只能随着尾数长度的增加而无限接近。而在计算机存储这样的数的时候就会用到舍入策略:
舍入策略
IEEE列出了四种不同的舍入方法:
- 舍入到最接近:舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式):会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中式以0结尾的)。
- 朝+∞方向舍入:会将结果朝正无限大的方向舍入。
- 朝-∞方向舍入:会将结果朝负无限大的方向舍入。
- 朝0方向舍入:会将结果朝0的方向舍入。
举个栗子
假如我们都采用朝+∞方向舍入,保留一位小数,那么0.41会入到0.5,0.41+0.41在计算机中就会计算成1.(仅仅是假设,实际计算机精度大得多)
而在我们let a = 0.3的时候会把0.3转换成2进制存储进来,再进行读取的时候,由于它和0.3的差比机器内部最大舍入误差小,所以再进行输出的时候就会正常输出0.3,在进行0.1+0.2的时候由于这种舍入误差的累积,导致了输出结果不等于0.3 。
不同语言的输出
1 | // C |
1 | // java |
1 | # python |
1 | // php |
1 | # ruby |
不同语言由于在输出长度方面有区别,所以最后的4可能会被截断。
浮点数比较
在日常使用中一般不建议浮点数进行比较,一种比较的方法是:
1 | abs(a - b) < 1e-8 |
参考链接
IEEE 754_wikipedia
深入理解计算机系统(原书第3版)
计算机组成原理-唐朔飞
js中浮点数的表示及计算–IEEE 754
原码补码反码移位
朱耀华_20180817