解释下 0.1+0.1-0.2=0 具体是怎么蒙对的,0.1+0.1+0.1-0.3 怎么就蒙不对了。
首先在 CPython 中浮点数总是64位的,这里为了书写方便,假设为32位的IEEE 754浮点数,计算的道理都一样,于是第一位是符号位,后面8位是阶码,最后23位是尾数。
0.1换算成二进制:
0.000110011001100110011001100110011001100110011001100110011001...
因为尾数只能有23位所以需要修约,IEEE 754定义的修约的规则可以有四种:取最靠近的(对于中间数0.5,IEEE 754默认修约成偶数,叫做 round half to even,譬如23.5和24.5都取为24),向上取整,向下取整,向靠近0取整。默认的取整规则是取就近的,这个很重要,直接决定了什么数能蒙对,什么数蒙不对。
0.1再换成以2为基数的幂数,尾数只取23位的话:

注意最后1100被round成了1101,因为第24位是一个1(于是进位,第23位的0变成1)。因为阶码有-127的移位(单精度),即使真正的2的指数加上127才是存储阶码,所以阶码应该为
01111111(127)-00000100(4) = 01111011
再加上尾数和符号位,整个32位(最左端为符号位)就为:
00111101110011001100110011001101,即3d cc cc cd
Intel的CPU都是little endian的,我们假设这里的CPU是Intel,所以在进程的虚拟内存中0.1被表示为cd cc cc 3d。
再看0.2,0.2换成位二进制:
0.001100110011001100110011001100110011001100110011001100110011...
因为0.2=0.1*2,所以就是把0.1向左移一位的结果,他和0.1不出意料地具有相同的尾数,只是阶码多了一位:

同理,这里阶码相当于0.1的阶码加一,于是就是01111011 + 00000001 = 01111100,同理,整个32位(最左端为符号位)就为:
00111110010011001100110011001101,在little endian的机器内存中就是cd cc 4c 3e。
那0.1+0.1时又发生了什么呢?两个浮点数相加时,遵循以下步骤:0 操作数检查
比较阶码大小并完成对阶
尾数求和运算
结果规格化(舍入处理)
首先两个数都不为0(回忆下0是怎么在浮点数中被表示的),阶码也相等,所以直接对尾数求和。两个尾数又相等,求和的结果相当于左移1位,结果是11.00110011001100110011010,溢出了,所以再右移1位以规格化(相当于小数点左移一位),阶码加1,右移的时候末尾的0被舍去,最后尾数变为1.10011001100110011001101,阶码是-4+1=-3,和0.2长得一模一样。
那如果0.2再加上一个0.1呢?这时候为了得出Python解释器给出的那个结果,我们需要真正使用双精度浮点数了(11位的阶码和52的尾数)。0.1的尾数和阶码分别为1.1001100110011001100110011001100110011001100110011010和-4,请注意尾数原本以110011001结尾,只不过最后这个1后面又是一个1,并且后面大于0.5,于是110011001加一变为110011010,这就是导致0.2+0.1和0.3不一样的根本原因。
0.2则具有相同的尾数和-3的阶码,做0.1和0.2的加法时,首先把0.1的尾数变为0.11001100110011001100110011001100110011001100110011010以对阶,然后把这个数再和1.1001100110011001100110011001100110011001100110011010相加,结果是
10.01100110011001100110011001100110011001100110011001110,这个数的尾数是53位的,而且又有溢出,所以应该右移一位,然后舍去最后两位,注意最后两位刚好是中间数,但是根据IEEE 754的默认rounding scheme,应该向前进位变成偶数,所以结果就是
1.0011001100110011001100110011001100110011001100110100(10被舍去),同时阶码加一变为-2。
再看0.3是怎么表示的,它的阶码和尾数分别为1.0011001100110011001100110011001100110011001100110011和-2。注意尾数结尾0011后面接着的本应是剩下0011的循环,小于0.5,所以无法进位,所以导致了0.3和0.2+0.1不一样,我们把0.3和0.2+0.1摆在一起,看看区别有多大:
0.2+0.1:
1.0011001100110011001100110011001100110011001100110100
0.3:
1.0011001100110011001100110011001100110011001100110011
也就是说只要0.3在最低位加一的话就和0.2+0.1一样了,而尾数的最低位是第52位,再乘上-2的阶码,就是2的负54次方,这个数刚好就是:5.551115123125783e-17。
浮点数运算揭秘
1740

被折叠的 条评论
为什么被折叠?



