0.1 + 0.2不等于0.3这是一个普遍的问题,例如在JS控制台输入将得到0.30000000000000004
在python的控制台也是输出这个数:
在C里面运行以下代码,指定输出小数位为57位:
printf("%.57f", 0.1 + 0.2);
将得到:
0.300000000000000044408920985006261616945266723632812500000
那我们的问题来了,为什么计算机计算的0.1加0.2会不等于0.3?
首先我们来看一下JS能够表示最大数是多少,如下所示,打印Number.MAX_SAFE_INTEGER和Number.MAX_VALUE:
JS能表示的最大整数为9e16,能表示的最大正数为1.79e308,这两个数是怎么得来的呢?先来看一下整数在计算机的存储方式。
我们知道计算机是使用二进制存储数据的,整数也是同样的道理,整数可以分成短整型、基本型、长整型,占用的存储空间分别为16位、32位、64位,如果操作系统是32位的,那么使用长整型将会慢于短整型,因为一个数它需要分两次取,而在64位的操作系统,一次就可以取到8个字节或64位的数据,所以使用长整型不会有性能问题。另外,32位的操作系统内存只能识别到2 ^ 32 = 4G,而现在的电脑内存动不动就是8G、16G,所以现在的电脑基本都是64位的,不比前几年。
32位有符号整型的存储方式如下图所示:
第一位0表示正数,1表示负数,剩下的31位表示数值,所以32位有符号整数最大值为:
2 ^ 31 – 1 = 2147483647
即21亿多,如果要表示全球人口,那么32位整型是不够的。同理,64位有符号整型能表示的最大值为:
2 ^ 63 – 1 = 9223372036854775807
这是一个19位数,mysql数据库的id字段就经常用长整型表示,那为什么JS能表示的最大整数只有16位,而不是19位呢?这个要先说一下浮点型在计算机的存储方式。
现在浮点型的存储实现基本按IEEE754标准,符点数分为单精度和双精度,单精度为32位,双精度为64位。
在十进制里面,一个小数如0.75可以表示成7.5 * 10 ^ -1,同样地在二进制里面,0.75可以表示成:
0.75 = 1.1 * 2 ^ -1
即0.75 = (1 + 1 * 2 ^ -1) * 2 ^ -1,其中幂次方-1用阶码表示,而1.1由于二进制整数部分都是1,所以去掉1留下0.1作为尾数部分(因为都是1点多的形式,所以这个1就没必要存了)。因此0.75在单精度浮点数是这样表示的:
注意阶码要加上一个基数,这个基数为2 ^ (n – 1) – 1,n为阶码的位数,32位的阶码为8位,所以这个基数为127,8位阶码能表示的最小整数为0,最大整数为255,所以能表示的指数范围为:(0 – 127) ~ (255 – 127)即-127~128,上面要表示指数为-1,需要加上基数127,就变成126,如上图所示。
而尾数为0.1,所以尾数的最高位为1,后面的值填充0.
反过来,如果知道一个二进制的存储方式,同样地可以转换成10进制,如上图的计算结果应为:
(1 + 1 * 2 ^ – 1) * 2 ^ (126 – 127) = 1.5 * 2 ^ -1 = 0.75
那么0.1又该如何表示成一个二进制呢?
由于0.75 = 1 * 2 ^ -1 + 1 * 2 ^ -2,刚好可以被二进制精确表示,那0.1呢?没办法了,0.1无法被表示成这种形式,只能是用另外一个数尽可能地接近0.1(同理1/3无法在10进制精确表示,但是可以在3进制精确表示,只是我们习惯了10进制)。
我们可以用一小段C代码来研究一下0.1被存储成什么了,如下代码所示:
void printBits(size_t const size, void const * const ptr)
{
unsigned char *b = (unsigned char*) ptr;
unsigned char byte;
int i, j;
for (i=size-1;i>=0;i--)
{
for (j=7;j>=0;j--)
{
byte = (b[i] >> j) & 1;
printf("%u", byte);
}
}
puts("");
}
double a = 0.1;
double b = 0.1;
printBits(sizeof(a), &a);
printBits(sizeof(b), &b);
因为C可以读取到原始的内存内容,所以可以打印每位的数据是什么。如上代码打印的结果如下:
双精度浮点数用11位表示阶码,52位表示尾数,如下图所示:
所以双精度的阶码基数为2 ^ 10 – 1 = 1023,0.1的阶码为01111111011,等于二进制1019,所以它的指数为-4:
尾数约为0.6:
由于这个精度不够,我们要找一个高精度的计算器,如笔者找的这个:
有了这个尾数之后,再让它乘以指数,得到结果为:
也就是说0.1的实际存储要比0.1大,大概大了5.5e-17.
注意到,0.2和0.1的区别在于0.2比0.1的阶码大了1,其它的完全一样。所以,0.2也是偏大了:
两个数相加的结果为:
但是注意到0.1 + 0.2并不是上面的结果,要比上面的大:
这又是为什么呢?因为浮点数相加,需要先比较阶码是否一致,如果一致则尾数直接相加,如果不一致,需要先对阶,小阶往大阶看齐,即把小阶的指数调成和大阶的一样大,然后把它的尾数向右移相应的位数。如上面的0.1是小阶,需要对它进行处理,如下:
需要把0.1的小数点向右移一位变成:
向右移一位导致尾数需要进行截断,由于最后一位刚好是0,所以这里直接舍弃,如果是1,那么尾数加1,类似于十进制的四舍五入,避免误差累积。现在0.1和0.2的阶码一样了,尾数可以进行相加减了,如下把它们俩的尾数相加:
可以看到,发生了进位,变成了53位,已经超过了尾数52位的范围,所以需要把阶码进一位,即指数加1,两数和的尾数右移一位,即除以2,由于尾数的最后一位是1,进行“四舍五入”,即舍弃最后一位后再加上1,最后尾数变成了如下图所示:
而指数加1,变成了-2,所以最后的计算结果为:
这个就和控制台的输出一致了,并且和C的输出完全一致。到此,我们就回答了为什么0.1加0.2不等于0.3了。上面还提出了两个问题,其中一个是:为什么JS的最大正数是1.79e308呢?这个数其实就是双精度浮点数所能表示最大正值,如下使用python的输出:
那为什么JS的最大正整数不是正常的64位的长整型所能表示的19位呢?因为JS的正整数是用的尾数的长度表示的,由于尾数是52位,加上整数的一位,它所能表示的最大的整数为:
为什么JS要用这种方式呢?因为JS的整型和浮点型在计算过程中可以随时自动切换,应该是考虑到了这个原因,所以才拿浮点型的大小限制来做整型大小的限制。
由于后端的数据库的ID字段可能会大于这个值,如果传来了一个很大的数,在调JSON.parse的时候将会丢失精度,ID就不对了,所以如果出现这种情况,应该让后端把ID当成字符串的方式传给你。
另外需要注意的是,双精度符点数的可靠位数为15位,也就是说从第16位开始可能是不对的,如0.1 + 0.2 = 0.30000000000000004,最后面的04这两位是不可靠的。
但是会有一种情况精度要求很高,15位精度会不够用,例如计算天体运算。那怎么办呢?有一种绝对精准的方式就是用分数表示,例如0.1 + 0.2 = 3/10,计算的过程和最后的结果都用分数表示,分数的结果,你需要精确到多少位都可以取到。这个在matlab/maple等科学计算软件都有实现。
最后怎么判断两个小数是否相等呢?用等号肯定是不行的了,判断两个小数是否相等要用它们的差值和一个很小的小数进行比较,如果小于这个小数,则认为两者相等,ES6新增了一个Math.EPSILON属性,如比较0.1 + 0.2是否等0.3应该这么操作:
作者:会编程的银猪
原文:http://www.renfed.com/2017/05/13/float-number/
---- 广告 ----
掘金是一个高质量的技术社区,从ECMAScript6到Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。点击链接即可访问掘金官网,或到各大应用市场搜索「掘金」下载APP,技术干货尽在掌握中