写在前面
之前面试遇到过一个问题:为什么JavaScript里面0.1+0.2 !== 0.3。当时我就回答到了浮点数精度有误差,显然面试官不满意,这不谁都知道吗
小红书上也强调过不要进行0.1+0.2 === 0.3的判断,仅仅提到如下理由
关于浮点数值计算会产生舍入误差的问题,有一点需要明确:这是使用基于IEEE754数值的浮点计算的通病,ECMAScript并非独此一家;其他使用相同数值格式的语言也存在这个问题
因此,也仅仅能答出这是因为ECMAScript使用了基于IEEE754标准的浮点数存储导致,但是深入计算机层面,又是为什么?
计算机角度
浮点数
计算机中数的表示有定点数和浮点数
- 定点数小数点位置确定。小数点在符号位后面成为小数定点机(这类机器只能表示小数),小数点在末尾成为整数定点机(这类机器只能表示整数)
- 浮点数表示类似于十进制的科学计数法表示。由阶数(阶码,阶符),尾数(尾码,尾符)构成。尾数的位数决定了浮点数的精度,阶数的位数决定了浮点数的范围
浮点数的规格化
- 对于小数来说,转换为二进制是乘2取整操作,例如将0.1转换成二进制
运算过程 |
取整 |
小数部分 |
0.1 * 2 = 0.2 |
0 |
0.2 |
0.2 * 2 = 0.4 |
0 |
0.4 |
0.4 * 2 = 0.8 |
0 |
0.8 |
0.8 * 4 = 1.6 |
1 |
0.6 |
0.6 * 2 = 1.2 |
1 |
0.2 |
... |
... |
... |
-
基于IEEE74标准的双精度浮点数(尾数最多52位),我们可以得到如下结果:
0.1=0.0001(1001100110011001100110011001100110011001100110011001)(52)
-
规格化处理
21≤∣S∣ <1
- 对于负数形式的补码,规格化的定义不适用(下面负数补码表示-1,不满足规格化定义)
S>0 |
规格化形式 |
S<0 |
规格化形式 |
真值 |
0.1XX...X |
真值 |
-0.1XX...X |
原码 |
0.1XX...X |
原码 |
1.1XX...X |
补码 |
0.1XX...X |
补码 |
1.0XX...X |
反码 |
0.1XX...X |
反码 |
1.0XX...X |
-
因此通常来说
对于原码,不论整数,负数,第一数位为1即为规格化
对于补码,符号位和第一数位不同为规格化
在计算机中我们通常使用异或电路,当符号位和第一数位不同时,表示规格化完成
IEEE754标准
- 数字存储格式:S(数符) + 阶码(含阶符) + 尾数
- 尾数为规格化表示
- 非“0”的有效位最高位为“1”(隐含)
|
符号位 S |
阶码 |
尾数 |
总位数 |
短实数(单精度) |
1 |
8 |
23 |
32 |
长实数(双精度) |
1 |
11 |
52 |
64 |
临时实数 |
1 |
15 |
64 |
80 |
0.1和0.2在计算机中是如何存储的
上面我们求出了0.1的二进制表示,同理可以求出0.2的(52表示尾数为52位)
0.1=0.00010.2=0.0011001100110011001100110011001100110011001100110011001(52)1001100110011001100110011001100110011001100110011001(52)
计算机中浮点数阶码(P)一般使用移码表示,尾数(S)使用补码表示,小数点前一个1做隐含处理。求得0.1和0.2的阶码和尾数如下
S(0.1)=1.10011001100...1(1100×12)S(0.2)=1.100110011...001(0011×12)P(0.1)=1,0000000100(−4)P(0.2)=1,0000000011(−3)
将阶数使用移码表示,存入计算机(括号中为隐含的1)
0.1⇒0:01111111100:10011001100110011001100110011001100110011001100110100.2⇒0:01111111101:10011001100110011001100110011001100110011001100110100.1=2−4×[1].10011001100110011001100110011001100110011001100110100.2=2−3×[1].1001100110011001100110011001100110011001100110011010
0.1+0.2在计算机中是如何运算的
首先需要対阶,小阶向大阶看齐(小阶的尾数减小,只需右移,损失精度而不会造成错误),这里阶差为1
注意这里作为小阶的0.1右移添补的是隐含的“1”,而不是默认右移添0
0.1=2−3×0.2=2−3×sum=2−3×10.1100110011001100110011001100110011001100110011001101(0)1.10011001100110011001100110011001100110011001100110100.0110011001100110011001100110011001100110011001100111
IEEE754标准对浮点数进行舍入时,一共定义了四种模型
Round to Nearest - roundTiesToEven (Default): 向最近的数靠近,最近的数需满足最低有效位为0或者偶数
Round toward 0: 向0靠近
Round toward +∞: 向正无穷靠近
Round toward −∞: 向负无穷靠近
第一种模型解决了50%的舍入情况,还有一种模型叫做Round to Nearest - tiesAwayFromZero,这种模型就和它的名字一样,远离0进行舍入
下面是5个舍入模型的例子,前四个模型用于IEEE 754标准下的浮点数舍入,第一个为默认模型
Mode / Example Value |
+11.5 |
+12.5 |
−11.5 |
−12.5 |
to nearest, ties to even (默认模型) |
+12.0 |
+12.0 |
−12.0 |
−12.0 |
toward 0 |
+11.0 |
+12.0 |
−11.0 |
−12.0 |
toward +∞ |
+12.0 |
+13.0 |
−11.0 |
−12.0 |
toward −∞ |
+11.0 |
+12.0 |
−12.0 |
−13.0 |
to nearest, ties away from zero |
+12.0 |
+13.0 |
−12.0 |
−13.0 |
这时候我们再来看sum规格化后,是如何使用上述标准进行舍入的
误差的产生
首先,sum做规格化,并隐含1后如下(sum此时位于a,b之间)
a=2−2×sum=2−2×b=2−2×1.0011001100110011001100110011001100110011001100110011(0)1.0011001100110011001100110011001100110011001100110011(1)1.0011001100110011001100110011001100110011001100110100(0)
按照上述第一个舍入模型,a的最低有效位为1,b的最低有效位为0,sum将使用b,然后存入计算机中。最后,我们将存入计算机中的0.3和sum进行一个比较
0.1+0.2=2−2×0.3=2−2×0.1+0.2⇒0:011111111010.3⇒0:011111111011.00110011001100110011001100110011001100110011001101001.0011001100110011001100110011001100110011001100110011:0011001100110011001100110011001100110011001100110[100]:0011001100110011001100110011001100110011001100110[011]
最终结果相差了2−2×2−52=2−54!!!
如果再将上面两个数转换成我们熟悉的十次方
0.1+0.2=0.3=0.300000000000000044408920985006...0.299999999999999988897769753748...
控制台输出一下
var ll = 0.300000000000000044408920985006;
var lll = 0.299999999999999988897769753748;
console.log(ll,lll);
总结
- 一道涉及JS基础数据结构—浮点数的问题,深入探究起来,复习了一波计算机组成原理知识
- 当时书上对IEEE754标准说得也很少,甚至不知道IEEE754的舍入标准。以至于之前的笔记我都认为sum的舍入使用的是学过的0舍1入法。直到写下这篇文章,揭开IEEE 754的面纱,才发现没那么简单
- 作为自己的第一篇文章,今后还有很多值得学习和努力的地方
参考
- Is floating point math broken? -- stack overflow(answer by Wai Ha Lee)
- Rounding floating-point numbers -- Wikipedia
- IEEE 754: Rounding Rules -- Wikipedia