这个系列是我读冴羽老师博客的感悟,
加入了个人的解读和练习题的解答
关于浮点数精度导致 js 的计算错误问题,我之前在Js 大数相加有提到过,那这篇文章将再次深入研究导致错误的原因。
ECMAScript 中的 Number 类型使用 IEEE754 标准来表示整数和浮点数值。
IEEE754 全称为 IEEE 二进制浮点数算数标准,这个标准定义了表示浮点数的格式等内容
在 IEEE754 中,规定了四种表示浮点数值的方式:单精确度(32 位)、双精确度(64 位)、延伸单精确度与延伸双精确度。
ECMAScript 采用的是双精确度,用 64 位字节储存一个浮点数
浮点数转二进制
1020 十进制:
1020 = 1 _ 10^3 + 0 _ 10^2 + 2 _ 10^1 + 0 _ 10^0
1020 二进制:
1020 = 1 _ 2^9 + 1 _ 2^8 + 1 _ 2^7 + 1 _ 2^6 + 1 _ 2^5 + 1 _ 2^4 + 1 _ 2^3 + 1 _ 2^2 + 0 _ 2^1 + 0 _ 2^0
我之前一直好奇用二进制表示小数是什么样的,因为计算器和网上的在线转二进制都不支持小数
但细想一下一定还是类似的,比如:
0.75 = a _ 2^-1 + b _ 2^-2 + c _ 2^-3 + d _ 2^-4 + …
在二进制里 abcd 只能是 0 或 1
要继续算出上面的 abcd 是 0 还是 1,我们在等号两边同时乘以 2
1 + 0.5 = a _ 2^0 + b _ 2^-1 + c _ 2^-2 + d _ 2^-3…
a * 2^0 = a, 由于 a 只能是 1 或 0,这里 a 应该是组成 1.5 里的最大值,所以 a 为 1
剩下的的是:
0.5 = b _ 2^-1 + c _ 2^-2 + d * 2^-3…
再次两边都乘以 2
1 + 0 = b _ 2^0 + c _ 2^-2 + d * 2^-3…
同上得出 b 也是 1,同时等号左边已经为 0 了,所以 0.75 的二进制就是 0.11
然而不是所有的数都像 0.75 这么好算,我们来算下 0.1:
|
|
然后你就会发现,这个计算在不停的循环,所以 0.1 用二进制表示就是 0.00011001100110011……
IEEE754 中的储存规则
IEEE754 的储存规则其实就是科学计数法:
正负 _ 进制的指数 _ 有一位个位的小数(x.x)
sign | Exponent | Mantissa |
---|---|---|
1 Bit | 11 Bits | 52 Bits |
我们知道-1020 十进制的科学计数法是 -1 _ 10^3 _ 1.02
那在计算机里我们只关注二进制,
0.1 的二进制是 0.00011001100110011……
科学计数法是 1 _ 2^-4 _ 1.1001100110011……
二进制的第三部分的个位一定是 1,在储存的时候能省位置就省,这个 1 就不存储了,只存小数点后的数字
由于第二部分的 Exponent 是可以为负数的(小数),11 位可以储存的范围是 0 ~ 2046,
但是要分正负,所有储存的范围是-1023 ~ 1023,
但是存负数很麻烦,所以用了一个办法:把 0 当作 1023,把 1023 当作 2046,所有数都加上 1023 即 0 ~ 2048,
当要用的时候再减去 1023(把 1023 记做 bias)
在这个标准下:
我们会用 1 位存储 S,0 表示正数,1 表示负数。
用 11 位存储 E + bias,对于 11 位来说,bias 的值是 2^(11-1) - 1,也就是 1023。
用 52 位存储 Fraction。
举个例子,就拿 0.1 来看,对应二进制是 1 _ 1.1001100110011…… _ 2^-4,
Sign 是 0,
E + bias 是 -4 + 1023 = 1019,1019 用二进制表示是 1111111011,
Fraction 是 1001100110011……
0.1 对应 64 个字节位的完整表示就是:
0 01111111011 1001100110011001100110011001100110011001100110011010
同理, 0.2 表示的完整表示是:
0 01111111100 1001100110011001100110011001100110011001100110011010
所以当 0.1 存下来的时候,就已经发生了精度丢失,当我们用浮点数进行运算的时候,使用的其实是精度丢失后的数
在运算时也会发生精度丢失,储存和运算都有可能发生超出位数精度丢失。