计算机底层浮点运行的误差

计算机底层存在浮点运行的误差?

计算机底层浮点运行的误差

计算机在进行浮点数运算时,由于浮点数的有限精度,会出现舍入误差。这是因为计算机使用有限的位数来表示实数,不能精确表示所有小数。以下是一个常见的浮点运算误差的例子:

例子:0.1 + 0.2 != 0.3

我们来看一个简单的 Python 示例:

1
2
3
4
5
6
a = 0.1
b = 0.2
c = a + b

print(c) # 输出 0.30000000000000004
print(c == 0.3) # 输出 False

解释:

  1. 问题的核心0.10.2 不能被计算机精确表示为二进制浮点数。计算机内部使用 IEEE 754 标准来表示浮点数,在这种表示法中,像 0.10.2 这样的十进制数没有精确的二进制表示,因此在存储它们时,计算机会引入微小的误差。

  2. 实际存储的值:虽然在代码中我们写的是 0.10.2,但计算机实际上存储的值是接近这些数的二进制近似值。

    • 0.1 实际上被表示为 0.10000000000000000555111512312578...
    • 0.2 实际上被表示为 0.20000000000000001110223024625156...
  3. 误差累积:当将这两个数相加时,误差会累积,因此 0.1 + 0.2 得到的结果不是 0.3,而是 0.30000000000000004

  4. 比较失败:在计算机中,0.3 也不能被精确表示,实际上被表示为 0.29999999999999998889776975374843...。因此,当你将 0.1 + 0.2 的结果与 0.3 进行比较时,二者的二进制表示不一样,导致 0.1 + 0.2 != 0.3

为什么会发生这种情况?

这是因为计算机使用二进制表示数,而某些十进制小数无法在二进制系统中精确表示。例如:

  • 在十进制中,1/3 是无限循环小数 0.3333...
  • 在二进制中,0.10.2 也是类似的无限循环小数。

浮点数在存储时必须被截断为有限的位数,因此无法精确表示,这会导致计算误差。

如何应对浮点误差?

  1. 使用误差范围进行比较:在比较两个浮点数时,可以使用一个允许的误差范围(如 epsilon)来避免误差带来的问题。

    1
    2
    3
    epsilon = 1e-9
    if abs((a + b) - 0.3) < epsilon:
    print("a + b 等于 0.3")
  2. 使用 decimal 模块:如果需要更高精度的计算,可以使用 Python 的 decimal 模块,它可以进行任意精度的十进制运算。

    1
    2
    3
    4
    5
    6
    7
    8
    from decimal import Decimal

    a = Decimal('0.1')
    b = Decimal('0.2')
    c = a + b

    print(c) # 输出 0.3
    print(c == Decimal('0.3')) # 输出 True
  3. 使用 fractions 模块:可以使用 fractions.Fraction 模块进行有理数运算,避免浮点误差。

    1
    2
    3
    4
    5
    6
    7
    8
    from fractions import Fraction

    a = Fraction(1, 10) # 0.1
    b = Fraction(2, 10) # 0.2
    c = a + b

    print(c) # 输出 3/10
    print(float(c)) # 输出 0.3

总结

浮点运算中的误差是由计算机使用有限位数来表示无限精度的实数造成的。了解浮点数的底层表示和局限性有助于我们编写更加健壮的数值计算程序,避免常见的误差问题。

————————————————————————————————————————

案例

当然,除了 0.1 + 0.2 != 0.3 这个经典的例子之外,还有其他常见的浮点数运算误差案例。下面是几个典型的例子:

浮点数精度丢失累积问题

在进行大量浮点运算时,精度丢失会逐渐累积,导致结果与预期相差甚远。

例子:重复累加

1
2
3
4
5
sum_value = 0.0
for i in range(1000000):
sum_value += 0.0001 #按理来说应该是0.0001*1000000=100

print(sum_value) # 输出 100.00000000219612

解释:

  • 理论上,0.0001 累加 1,000,000 次应该等于 100,但实际上输出是 100.00000000015625,比预期值多了一点点。
  • 这是因为在每次累加过程中,浮点数的微小舍入误差逐渐累积,导致最终的结果与预期值有差异。

浮点数舍入误差

某些浮点数在运算过程中,舍入误差会导致最终结果不精确。

例子:除法运算

1
2
3
4
5
a = 1.0
b = 3.0
result = a / b

print(result) # 输出 0.3333333333333333

解释:

  • 理论上 1 / 3 应该是 0.333...,但由于计算机存储位数有限,结果被截断为 0.3333333333333333
  • 这是因为浮点数无法精确表示无限小数,计算机会在某个点舍入,从而引入误差。

大数和小数运算引发的精度丢失

当一个很大的数和一个很小的数相加时,较小的数可能会被忽略,导致结果失去精度。

例子:大数与小数相加

1
2
3
4
5
a = 1e16  # 一个非常大的数
b = 1.0 # 一个很小的数
result = a + b

print(result) # 输出 1e16, b 被忽略了

解释:

  • 理论上,a + b 应该是 1e16 + 1 = 10000000000000001,但由于 1e16 远大于 1.0,浮点数无法表示这样的精度差异,因此 b 被忽略了,结果仅输出 1e16
  • 这种误差在处理非常大或非常小的数时尤其明显。

浮点数的非对称性

浮点数在加法和减法时,结果可能表现出非对称性,导致不可逆的运算。

例子:浮点数减法的非对称性

1
2
3
4
5
6
a = 1.0000001
b = 1.0
c = a - b

print(c) # 输出 1.0000000005838672e-07
print(c + b) # 输出 1.0000001000000001

解释:

  • c = a - b 的结果应该是 0.0000001,但由于浮点数精度的限制,计算结果是 1.0000000005838672e-07
  • 当你将这个结果再加上 b,你会发现得不到原来的 a = 1.0000001,而是稍大一些的 1.0000001000000001,这说明浮点数的加减法是非对称的,存在舍入误差。

比较浮点数的陷阱

由于浮点数的误差问题,直接比较浮点数时可能得不到预期的结果。

例子:浮点数比较

1
2
3
4
5
6
a = 0.3 * 3
b = 0.9

print(a) # 输出 0.8999999999999999
print(b) # 输出 0.9
print(a == b) # 输出 False

解释:

  • 理论上,0.3 * 3 应该等于 0.9,但是由于浮点数的误差,0.3 * 3 的结果是 0.8999999999999999,所以 a == b 的结果是 False
  • 这是由于浮点数的表示精度有限,乘法运算中引入了微小的舍入误差,导致直接比较结果不相等。

三角函数计算中的误差

浮点数运算不仅仅出现在简单的加减乘除,甚至在三角函数等更复杂的计算中也会有误差。

例子:三角函数的误差

1
2
3
4
5
6
import math

x = math.cos(math.pi / 3) # cos(60°)

print(x) # 输出 0.5000000000000001
print(x == 0.5) # 输出 False

解释:

  • 理论上 cos(60°) 应该等于 0.5,但是实际输出是 0.5000000000000001,比预期多了一点。
  • 这是因为 math.cos() 计算时使用的浮点数表示存在精度限制,因此导致了微小的误差。
  • 同样的,直接比较 x == 0.5 会返回 False,因为二者并不完全相等。

浮点数溢出和下溢

当浮点数过大或者过小时,计算机会产生溢出或下溢(即精度丢失为零)。

例子:溢出和下溢

1
2
3
4
5
6
7
8
9
10
11
import sys

# 浮点数上限
large_num = sys.float_info.max
small_num = sys.float_info.min

# 溢出例子
print(large_num * 2) # 输出 inf(无穷大)

# 下溢例子
print(small_num / 2) # 输出 0.0

解释:

  • 当浮点数超出最大表示范围时,计算机会发生溢出,并返回 inf(无穷大)。
  • 当浮点数接近最小表示范围,并且除以一个较大的数时,计算结果会被下溢为 0.0,这是由于计算精度不够导致的。

总结

浮点数运算中的误差主要是由于计算机在底层使用有限位数来表示实数。在进行复杂运算时,舍入误差会累积,导致结果与预期不符。因此,在数值计算中:

  • 尽量避免直接比较浮点数。
  • 使用误差容忍范围(如 epsilon)进行比较。
  • 对需要高精度计算的场景,建议使用 decimalfractions 模块。

这些例子展示了浮点数运算中的常见误差,并提醒我们在设计数值算法时需要考虑这些潜在问题。