关于python:为什么浮点数运算会产生误差

38次阅读

共计 2325 个字符,预计需要花费 6 分钟才能阅读完成。

前言

大家在写代码时都会遇到所谓的浮点误差,如果你还没踩过浮点误差的坑,只能说你太侥幸了。

以下图的 Python 为例,0.1 + 0.2 并不等于 0.38.7 / 10 也不等于 0.87,而是 0.869999…,真是太奇怪了 🤔

但这相对不是什么阳间 bug,也不是 Python 设计得有问题,而是浮点数在做运算时必然的后果,所以即使是在 JavaScript 或其余语言中也都是一样:

电脑是怎么贮存一个整数的(Integer)

在讲为什么会存在浮点误差之前,先来谈谈电脑是怎么用 0 跟 1 来示意一个 整数 的,大家应该都晓得二进制:例如 101 代表 $2^2 + 2^0$ 也就是 5、1010 代表 $2^3 + 2^1$ 也就是 10。

如果是一个无符号的 32 bit 整数,代表它有 32 个地位能够放 0 或 1,所以最小值就是 0000...0000 也就是 0,而最大值 1111...1111 代表 $2^{31} + 2^{30} + … + 2^1 + 2^0$ 也就是 4294967295

从排列组合的角度来看,因为每一个 bit 位都能够是 0 或 1,整个变量的值有 $2^{32}$ 种可能,所以能够 准确的 表白出 0 到 $2^{23} – 1$ 之间的任一个值,不会有任何误差。

浮点数(Floating Point)

尽管从 0 到 $2^{23} – 1$ 之间存在很多个整数,但其数量究竟是 无限 的,就是 $2^{32}$ 那么多个而已;但浮点数就不同了,咱们能够这样想:在 1 到 10 这个区间中只有十个整数,却有 无穷多个 浮点数,例如 5.1、5.11、5.111 等等,怎么也列举不完。

但因为在 32 bit 的空间中就只有 2³² 种可能性,为了把所有浮点数都塞在这个 32 bit 的空间外面,许多 CPU 厂商创造了各种浮点数的示意形式,但如果每家 CPU 的格局都不一样也很麻烦,所以最初是以 IEEE 公布的 IEEE 754 作为通用的浮点数运算规范,当初的 CPU 也都遵循这个规范进行设计。

IEEE 754

IEEE 754 外面定义了很多货色,其中包含单精度(32 bit)、双精度(64 bit)和非凡值(无穷大、NaN)的示意形式等等

规格化

以 8.5 这个浮点数来说,如果要变成 IEEE 754 格局的话必须先做一些规格化解决:把 8.5 拆成 8 + 0.5 也就是 $2^3 + (\cfrac{1}{2})^1$,接着写成二进位变成 1000.1,最初再写成 $1.0001 \times 2^3$,与十进制的迷信记数法很类似。

单精度浮点数

在 IEEE 754 中 32 bit 浮点数被拆分成三个局部,别离是 数符(sign)、阶码(exponent)和尾数(fraction),加起来总共是 32 个 bit

  • 数符(sign):最左侧的 1 bit 代表正负号,负数的话 sign 就为 0,反之则是 1
  • 阶码(exponent):两头的 8 bit 代表规格化之后的次方数,采纳的是 阶码真值 +127 的格局,也就是 3 还要再加上 127 等于 130
  • 尾数(fraction):最右侧的 23 bit 放的是小数局部,以 1.0001 来说就是去掉 1. 之后的 0001

所以如果把 8.5 示意成 32 bit 格局的话应该是这样:

什么状况下会产生误差?

后面举的 8.5 的例子能够示意为 $2^3 + (\cfrac{1}{2})^1$,是因为 8 和 0.5 刚好都是 2 的次方数,所以齐全不会产生任何精准度问题。

但如果是 8.9 的话因为没方法换成 2 的次方数相加,所以最初会被迫示意成 $1.0001110011… \times 2^3$,而且还会产生大略 $0.0000003$ 的误差,如果对后果好奇的话能够到 IEEE-754 Floating Point Converter 网站上玩玩看。

双精度浮点数

后面所讲的单精度浮点数只用了 32 bit 来示意,为了让误差更小,IEEE 754 也定义了如何用 64 bit 来示意浮点数,跟 32 bit 比起来 fraction 局部扩充了两倍多,从 23 bit 变成 52 bit,所以精准度天然会进步许多。

以方才的 8.9 为例,用 64 bit 示意的话尽管能够变得更准,但因为 8.9 无奈齐全写成 2 的次方数相加,到了小数下 16 位依然会呈现误差,不过与单精度的误差 0.0000003 比起来曾经小了很多

相似的状况还有像 Python 中的 1.00.999...999 是相等的、123122.999...999 也是相等的,因为他们之间的差距曾经小到无奈放在 fraction 外面,所以从二进制格局看来它们每一个二进制位都是一样的。

解决办法

既然浮点数的误差是无奈防止的,那就只好跟它共处了,上面是两个比拟常见的解决办法:

设定最大容许误差 ε (epsilon)

在某些语言会提供所谓的 epsilon,用来让你判断是不是在浮点误差的容许范畴内,以 Python 来说 epsilon 的值大概是 $2.2e^{-16}$

所以你能够把 0.1 + 0.2 == 0.3 改写成 0.1 + 0.2 — 0.3 <= epsilon,这样就能防止浮点误差在运算过程中捣鬼,正确的比拟出 0.1 加 0.2 是不是等于 0.3 了。

当然如果零碎没提供的话你也能够本人定义一个 epsilon,设定在 2 的 -15 次方左右

齐全应用十进制进行计算

之所以会有浮点误差,是因为把十进制转为二进制的过程中没方法把所有的小数局部都塞进了尾数中,既然转换可能会有误差,那罗唆就不转了,间接用十进制来做运算。

在 Python 外面有一个 module 叫做 decimal,在 JavaScript 中也有相似的包。它能够帮你用十进制来进行计算,就像你本人用纸笔计算 0.1 + 0.2 相对不会出错、也不会有任何误差。

尽管用十进制进行计算能够完全避免浮点数的误差,但因为 Decimal 的十进制计算是模仿进去的,在最底层的 CPU 电路中还是在用二进制进行运算,执行起来会比原生的浮点运算慢很多,所以不倡议所有的浮点运算都用 Decimal 来进行。

正文完
 0