为什么计算机表示浮点数会有误差

1,692 阅读4分钟

计算机使用定点格式和浮点格式来表示数值,浮点数无法精确地表示数值,导致在浮点数运算的过程中会有误差,比如:

>>> 0.1 + 0.2 == 0.3
False

计算机的浮点数运算误差和计算机底层的二进制表示方式有关,无论使用什么语言来进行浮点数运算,都会遇到精度的问题。

要解决精度问题,可以通过模拟运算或者允许很小范围的误差。

定点数和浮点数

顾名思义,定点数是指小数点位置是固定的,浮点数是指小数点位置是不固定的。

定点数

用定点数表示整数的时候,小数点的位置是固定在最右边的。用定点数表示小数的时候一般小数点固定在最左边。 幻灯片2.png

计算机能够准确地表示一定范围内的整数。整数分为无符号整数和有符号整数,对于使用n位存储的无符号整数,最小整数所有位为0,最大整数所有位为1,所以能存储的整数范围是0到i=0n12i=2n1\sum^{n-1}_{i=0}2^i=2^n-1;一般所有的机器都是以补码的形式表示有符号整数的,对于使用n位存储的有符号整数,最高位是符号位,符号位为1表示负数,符号位位0表示正数,对应的十进制的整数是xn12n1+i=0n2xi2i-x_{n-1}2^{n-1} + \sum^{n-2}_{i=0}x_i2^i,最小负数符号位为1其余位为0,最大正数符号位为0,其他位为1,所以能存储的整数范围是2n1-2^{n-1}i=0n22i=2n11\sum^{n-2}_{i=0}2^i=2^{n-1}-1

浮点数

定点数能表示的数的范围是有限的,为了能表示更大范围的数字,需要使用浮点数。 幻灯片3.png

使用浮点数表示时,数字被表示为N=MREN = M \cdot R^E,其中M是尾数,E是阶码,R是阶码的基数,R为2,随着M和E的值的不同,小数点的位置是变化的(浮动的)。

这里只简单描述IEEE浮点标准,以了解为什么浮点数无法精确地表示数值,其实数值被编码的方式比本文所描述的要复杂一些。

IEEE浮点标准中数的表示形式是:

   V=(1)s×M×2E  V = (-1)^s \times M \times 2^E

其中s是符号,E是阶码,M是尾数。

IEEE浮点标准中的规格化的32位(单精度)浮点数的表示为:

   V=(1)s×(1.M)×2(E127)  V = (-1)^s \times (1.M) \times 2^{(E-127)}

这个公式对应E的值既不是全为0,又不是全为1时的情况,127是偏置,等于28112^{8-1}-1,8是E的位数。

非规格化的单精度浮点数表示为:

 V=(1)s×(M)×2(E127) V = (-1)^s \times (M) \times 2^{(E-127)}

这个公式对应的E值全为0的情况。

因为对于一定范围内的整数,都可以使用i=0n1xi2i\sum^{n-1}_{i=0}x_i2^ixn12n1+i=0n2xi2i-x_{n-1}2^{n-1} + \sum^{n-2}_{i=0}x_i2^i准确表示出来,所以定点数能准确地表示一定范围内的整数,但是对于一定范围内的实数,有的数是不可以使用(1)s×M×2E(-1)^s \times M \times 2^E这种形式表示的,所以浮点数无法准确地表示数值,无法被该公式表示的值只能被近似地表示。

精度问题的解决方法

以Python语言为例,使用Python进行浮点数运算时,也会遇到浮点数精度的问题。

>>> 0.1
0.1

这里打印出的内容是0.1是Python自己做了四舍五入,打印出的内容不是实际存储在计算机的内容。在计算机存储中,是不能精确表示110\frac{1}{10}的,存储的是一个接近110\frac{1}{10}的值:(1)0×3602879701896397×255(-1)^0\times3602879701896397\times2^{-55},即0.1000000000000000055511151231257827021181583404541015625,当我们输出0.1的时候,直接输出存储在机器中的精确值,是非常不直观的,所以会通过四舍五入直接输出0.1。想要知道一个浮点数的精确的值的时候,可以使用float.as_integer_ratio()方法。

>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)

在机器存储中,0.10.2都不能被精确的表示,都有误差,两者相加后也会有误差。

>>> 0.1 + 0.2 == 0.3
False

模拟计算

对于需要精确的十进制表示的情况,可以使用 decimal模块,它实现了十进制运算,适用于记账应用等需要高精度数据的应用。

>>> from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True

还可以使用实现了基于有理数的运算的 fractions模块。

>>> from fractions import Fraction
>>> Fraction(1, 10) + Fraction(1, 5) == Fraction(3, 10)
True

允许很小范围的误差

自己定义一个误差的值,比如0.0000001,小于这个误差就认为没有误差。

>>> def __eq__(a, b):
...     return (a - b) < 0.0000001
... 
>>> __eq__(0.1 + 0.2, 0.3)
True

或者使用math包的isclose函数:

>>> from math import isclose
>>> isclose(0.1 + 0.2, 0.3)
True