为什么浮点运算不精确
验证
>>> 0.3-0.2
0.09999999999999998
>>> 0.2-0.1
0.1
深入一点验证
首先写工具。写一个函数来显示64位8字节的二进制数据。
import struct
def byte2bin(s, g=8):
o = []
for i in range(0, len(s), g):
sub = s[i:min(i+g, len(s))]
o.append(' '.join((f'{c:08b}' for c in sub)))
return '\n'.join(o)
print(byte2bin(b'abcdefghijklmnopqrstuvwxyz'))
输出如下。
01100001 01100010 01100011 01100100 01100101 01100110 01100111 01101000
01101001 01101010 01101011 01101100 01101101 01101110 01101111 01110000
01110001 01110010 01110011 01110100 01110101 01110110 01110111 01111000
01111001 01111010
随后将数据转换为二进制数据,注意大小端问题。
print(byte2bin(struct.pack('>h', 16385)))
print(byte2bin(struct.pack('>h', -16383)))
输出如下。
01000000 00000001
11000000 00000001
最后再次验证。
def show_double(d):
print(d)
print(byte2bin(struct.pack('>d', d)))
a = 0.1
show_double(a)
b = 0.2
show_double(b)
c = b-a
show_double(c)
print(c == a)
d = 0.3
show_double(d)
e = d-b
show_double(e)
print(e == a)
输出如下。
0.1
00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011010
0.2
00111111 11001001 10011001 10011001 10011001 10011001 10011001 10011010
0.1
00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011010
True
0.3
00111111 11010011 00110011 00110011 00110011 00110011 00110011 00110011
0.09999999999999998
00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011000
False
可以见到,0.2-0.1的结果在二进制上和0.1无区别。0.3-0.2的结果在二进制上和0.1有显著区别。0.1的末尾为1010,0.3-0.2的结果为1000。
浮点数的原理
二进制浮点数和十进制小数的关系,就如同二进制整数和十进制整数的关系。
- 整数表示系统里,最左边表示数字的大小,从0-9。当需要表示"10"这个概念时,实际上是将0-9写在左边第二位,来表示有多少个"10"。以此类推。
- 小数表示系统里,最右边的数字表示有多少个"1/10"。以此类推。
为什么要这么表示?因为这样表示的时候,“位移运算”和“乘以10”就联系起来了。而且加减法都可以按位运算。
- 909*10 = 9090
- 909左移一位 = 1010
- 909+101 = 1010,具体运算为(9+1), (0+0), (9+1),最后从右向左顺序进位
- 90.9*10 = 909
- 90.9+99.99 = 190.89,具体运算为(9+9),(0+9),小数点,(9+9),(0+9),最后从右向左顺序进位
- 除了小数点位对齐外,小数加法和整数加法没有任何特别之处
90.9+99.99 = 190.89
的竖式表达
90.90
99.99
------
190.89
同理,二进制整数和浮点数也具有相应规律。
- 0b101*0b10 = 0b1010
- 0b101左移一位 = 0b1010
- 0b101+0b11 = 0b1000,具体运算为(1+0), (0+1), (1+1),最后从右向左顺序进位
- 0b101.101*0b10 = 0b1011.01。注意,0b101.101在计算机中不是合格的表示法,但读者应当可以理解其意思
- 0b101.101+0b110.11 = 0b1100.011
- 上面的式子可能不大好算,我们换回10进制看看?5.625+6.75=12.375,完全正确。
0b101.101+0b110.11 = 0b1100.011
的竖式表达
0b101.101
0b110.110
----------
0b1100.011
深入浮点数的表示
请自行参阅这里。
def scale_ieee754d():
# https://zh.m.wikipedia.org/zh/IEEE_754
print('seeeeeee eeeeffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff')
def show_double(d):
print(d)
scale_ieee754d()
print(byte2bin(struct.pack('>d', d)))
输出如下(略去了相等判断)。
0.1
seeeeeee eeeeffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff
00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011010
0.2
seeeeeee eeeeffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff
00111111 11001001 10011001 10011001 10011001 10011001 10011001 10011010
0.1
seeeeeee eeeeffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff
00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011010
0.3
seeeeeee eeeeffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff
00111111 11010011 00110011 00110011 00110011 00110011 00110011 00110011
0.09999999999999998
seeeeeee eeeeffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff
00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011000
可见,0.2和0.1的尾数部分严格一致,两者差别只有指数部分。0.3和两者完全不同。
浮点数的减法竖式运算
首先还是要有工具函数。
def str2bin(s):
b = eval('0b'+s.replace(' ', ''))
return struct.unpack('>d', struct.pack('>Q', b))[0]
print(str2bin('00111111 11010011 00110011 00110011 00110011 00110011 00110011 00110011'))
随后我们来看0.3-0.2。
seeeeeee eeeeffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff
00111111 11010011 00110011 00110011 00110011 00110011 00110011 00110011
00111111 11001001 10011001 10011001 10011001 10011001 10011001 10011010
这两个数可以反向丢回str2bin里验证是否正确。
第一步回归完整形态。所谓“完整形态”,是因为规约形态下首位为1。所以运算前先要补回这个1。
seeeeeee eeee fffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff
00111111 1101 10011 00110011 00110011 00110011 00110011 00110011 00110011
00111111 1100 11001 10011001 10011001 10011001 10011001 10011001 10011010
第二步对齐。0.3的指数部分比0.2的大了一点儿。
seeeeeee eeee fffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffff
00111111 1101 10011 00110011 00110011 00110011 00110011 00110011 00110011
00111111 1101 01100 11001100 11001100 11001100 11001100 11001100 110011010
第三步尾数部分减法。
seeeeeee eeee fffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffff
00111111 1101 10011 00110011 00110011 00110011 00110011 00110011 00110011
00111111 1101 01100 11001100 11001100 11001100 11001100 11001100 110011010
--------------------------------------------------------------------------
00111111 1101 00110 01100110 01100110 01100110 01100110 01100110 011001100
第四步升位补齐。因为运算结果的最高位不为1,所以需要升位对齐。
seeeeeee eeee fffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffff
00111111 1101 00110 01100110 01100110 01100110 01100110 01100110 011001100
--------------------------------------------------------------------------
seeeeeee eeee fff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffff
00111111 1011 110 01100110 01100110 01100110 01100110 01100110 011001100
第五步换回归约形态,省略最高位的1,并且末尾补0。
seeeeeee eeee fff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffff
00111111 1011 110 01100110 01100110 01100110 01100110 01100110 011001100
------------------------------------------------------------------------
seeeeeee eeee ff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffff
00111111 1011 10 01100110 01100110 01100110 01100110 01100110 011001100
------------------------------------------------------------------------
seeeeeee eeee ffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffff
00111111 1011 1001 10011001 10011001 10011001 10011001 10011001 10011000
下面,我们把上面0.3-0.2的计算结果搬下来,再对照刚刚人工算出来的结果。
seeeeeee eeeeffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff
00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011000
-----------------------------------------------------------------------
00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011000
一模一样。
精度问题的原因
首先我们看到,上述减法操作做完后,最后末尾的0是补进去的。即最后得到数据的精度,其实不足以填充有效的尾数空间。
这是因为浮点模式下,数据只提供“最主要部分”的数据。大致逻辑就是,当一个数据有200位的时候,最后一位是0还是5并不重要。只要前面190位对就行了。
这种情况下,两个接近的大数相减,就容易得到一个“精度不足”的差。例如:
10000002.0-10000001.0 = 1.0
,很好理解。10000000002.0-10000000001.0 = 1.0
,也很好理解。10000000000000002.0-10000000000000001.0 = 2.0
,完全无法理解。
实际上,使用浮点数进行计算,只要前面的0足够多,最后的细微数据部分(都不必是小数)一定会出现问题。
结论
- 浮点数运算的结果,判断相等的时候要用“两者差小于一顶值”的办法。
- 算钱请务必用decimal。万一你哪天被派去给央行写代码呢?