关于浮点数计算时的精度问题 0.1+0.2不等于0.3

“未经省察的人生没有价值。”

问题

上个月同事遇到个 Bug,两个看起来应该相等的整型,对比结果却是 false。最后发现,在一连串算数过程中,混入了一个浮点型,于是误差就开始累积了。1.0000000000000001 不等于1 。0.1 + 0.2 不等于 0.3。


分析

项目要尽量避免使用浮点数?

浮点的误差来自于十进制小数跟二进制小数之间的转换。如果转换不是必须的或者没有精度要求,那就完全可以使用浮点数。
为什么在项目中要尽量避免使用浮点数,不使用浮点数,那该如何计算浮点数?

计算机内部是怎么存储数字的?

都是采用二进制存储。无论什么数据,在计算机内存中都是以01存储的,浮点数也不例外。

十进制和二进制怎么转换?

十进制整数转二进制整数:短除法,除2取余,直到商取0,自下而上读数,从左到右填数。
十进制小数转二进制小数:乘2取整,直到精度要求的位数,自上而下,从左到右填数。
十进制转换成二进制及二进制转换成十进制是如何转换的?

n bit 可以存储多少个整数?

2 的 n 次方。很简单,用 n 位用来放1或者0两种值,那就是有 2 的 n 次方个组合用来表示不同的数字。

科学计数法是怎样的?

在使用或运算某个占用位数较多的的数时,使用科学计数法省略掉一部分空间和时间。
a*10^n : 1<=|a|<10,n为整数,10可以替换成其他进制。为什么a的最大值小于10,因为大于10,就归到n+1去了。
也可表示成 aE+n、aE-n。

小数在计算机中是怎么存储的?

按照 IEEE 754 标准,又称 IEEE 二进制浮点数算数标准。
常用的有单精度(32位)、双精度(64位)。
V= (-1)^s * M * (2^E)

64Bits 把小数分为 3 个部分存储:

  • sign(S,符号):表示正负号,0为正,1为负(1 bit)。
  • exponent(E,指数):表示次方数(11 bits)。取值范围为 2^11 = 2048,表示范围为 0~2047。但是指数可以是负数,因此把取值一分为二,一半用来表示负数。于是就有了中间值为 2^(e-1)。二进制转十进制时,指数的值要加上中间值,反之则减去。
  • mantissa(M,尾数):表示精确度(有效数字) 1<=M<2(53 bits)。其实 M 只有52 bits 的空间,但是二进制的第一位有效数字必然是1,所以省去第一位有效数字,所以可以保存的有效数字为 52 + 1 = 53 位。

浮点数怎么做运算?

对阶:使两数的阶数相同。小阶向大阶看齐。
尾数求和:常规的二进制算数。
规格化和舍入:规格化,多出来的低位0舍1入,正是这里会导致精度丢失。
JavaScript 浮点数之迷:0.1 + 0.2 为什么不等于 0.3?

非规格化浮点数 Denormalized Number

简而言之就是原本规格化浮点数规定小数点第一位默认是1,但是非规格化浮点数允许是0,因此可以表示更大的精度,即 0.00001e-126。
非规格化浮点数
为什么将 0.1f 改为 0 会使性能降低 10 倍?


其他参考&工具

IEEE 754 Calculator
IEEE 754浮点数标准中64位浮点数为什么指数偏移量是1023?
js精度丢失问题-看这篇文章就够了(通俗易懂)
漫话:如何给女朋友解释为什么计算机中 0.2 + 0.1 不等于 0.3 ?


总结

  • JavaScript 浮点数之迷:0.1 + 0.2 为什么不等于 0.3? 这篇文章的结构非常好,以后可以模仿这篇来写。
  • 理解加整理,断断续续花了一周的碎片时间。不太好理解,内容也不少,经常昨天刚理解,今天再看,要重新理解。所以下次在理解的过程中,就应该把当时的理解做下笔记,最后再统一归纳整理才是。
  • 我好笨丫。