【译文】浮点数是怎么回事?

1,647 阅读9分钟

前言


  我猜想大多数读到这篇文章的人,都曾经在编程中有意无意地使用过浮点数字。当遇到一个简单的计算问题,抛出了意料之外的错误的时候,很多人试图寻找错误的原因,却往往不可得。例如:

0.1 + 0.2
// 实际结果是0.30000000000000004
// 我们想当然会认为是0.3

  这种预期的结果和实际请求有细微的差别的情况,往往让我们感到很沮丧。究其原因,实际上是因为我们不理解什么是浮点数。我写这篇文章的目的是为了澄清浮点数是什么我们为什么要使用浮点数,以及浮点数是如何工作的

我们为什么需要浮点数呢?


  计算机需要存储数字,现在的眼光看来这是一个显而易见的事实。这里的计算机是广义上的,不局限了个人电脑,还包括了手机、冰箱等现代化电器。手机、电脑显示当前时间、冰箱显示当前温度,这些事实都说明了计算机需要存储数字,来表示相应的信息。而这些数字实际上是以二进制的形式保存在计算机上面的文件里的。

  我希望大多数读到这篇文章的人都对二进制数有一定的了解,如果没有,可以考虑阅读Linda Vivah的这篇博文

  但小数呢?分数、π、实数呢?

对于任何有用的计算,我们需要计算机能够表示以下内容:

  • 非常非常非常非常小的数字
  • 非常非常非常非常大的数字
  • 以上两者之间的任何数字

从非常非常小的数字开始,让我们朝着正确的方向发展;我们如何在计算机上存储它们?

嗯,这很简单。我们用相当于十进制的二进制表示法来存储这些......

二进制分数


  举个例子,让我们选择一个随机的二进制分数: 101011.101

img

  这与十进制数字的工作方式非常相似。唯一的区别是,我们的基数使用的是2而不是10。如果你把上面的内容放入你所选择的二进制转换器中,你会发现它是正确的。

  那么我们如何存储这些二进制分数呢?

  假设我们分配一个字节(8位)来存储我们的二进制分数。00000000. 然后我们必须选择一个地方来放置我们的二进制分隔符,这样我们就可以拥有二进制数的分数部分。

  让我们试试在中间的位置!

  那我们能用这个字节表示的最大数字是多少?

  这... ... 也不是很厉害,很拉胯。也不是我们能代表的最小的数字,0. 00625。这里有大量浪费的存储空间,而且可能的数字范围很小。问题在于,选择任何地方放置我们的点都会使我们要么得到小数精度,要么得到一个更大的整数范围ーー而不是两者兼得。

  如果我们可以根据需要移动分数点,我们可以从有限的存储空间中得到更多。如果这个点能够像我们需要的那样漂浮起来,如果你愿意的话,一个浮点... ..。

那么什么是浮点数呢?


  浮点数正是一个浮动(分数)点的数字,它使我们有能力改变数字的相对大小。那么我们如何用数学的方式来表示一个数?

  1. 存储我们要表示的数字的有效数字(例如0.00000012中的12)。
  2. 知道小数点与有效数字的关系(如:0.00000012中的所有0) ?

  为了做到这一点,让我们穿越到初中,重温科学计数法。

科学计数法


  有谁还记得 mathsisfun 吗?我不知怎么感觉现在老了,但无论如何,这是从他们的网站,拿来的图片。(推荐这个网站,国内志愿者翻译了部分,建立了中文站)。

img

你可以用二进制做完全相同的事情! 取代 7 * 10^2 = 700。我们可以这样写:

1010111100 * 2^0 = 700

= 10101111 * 2^2 = 700

这相相当于 175 * 4 = 700。这是一个很好的方法来表示一个数字,它基于有效数字和小数点相对于有效数字的位置。

就是这样! 浮点数是一种二进制标准形式的数字表示。

如果我们想把这个表示正式化一点,我们需要考虑正数和负数。要做到这一点,我们还要在数字上加一个符号,乘以 ±1。

(正负号) ∗ (有效数字) ∗ (基数)^(指数)

回到 mathsisfun 给出的例子。

如果你正在阅读其他文献,你会发现这个表达看起来像。

S 是符号位,c 是有效数字,b 是基数,e 是指数。

我们为什么得到一些奇怪的错误?


  所以,我们现在知道什么是浮点了。但是为什么我不能在没有错误的风险下,做一些简单的事情呢?比如把两个数字加在一起。这个问题部分在于计算机,但主要的原因在于数学本身。

循环小数


  循环小数是数字表示系统中一个有趣的问题。让我们选择任意的分数 x/y,如果 y 的质数因子不是基数(比如10进制中的10)的质数因子,那么分数 x/y 转化为小数就是一个循环小数。

​ 这就是为什么像 1/21 这样的分数不能用有限的小数来表示的原因,21的质数因子有3和7,但是3和7都不是基数10的质数因子。所以1/21转化为小数是一个循环小数。

​ 让我们研究一个十进制的例子。

十进制

​ 假设你想把1/3和2/3这两个数字相加,我们都知道答案是1,不是42(银河系漫游指南的梗)。但如果我们是在十进制中工作,这就不那么明显了。

那是因为 1/3 = 0.3333333333333......

它不是一个可以用十进制的有限位数来表示的数字。由于我们不能存储无限位数,所以我们存储的是精确到10位的近似数。那么这个计算就变成了

0.333333333333 + 0.666666666666 = 0.999999999999

计算出来的结果当然不是1。它即使非常非常靠近1,但是并不是1。

我们可以存储数字的有限性与我们不能简单地用有限数字表示所有数字这一不可避免的事实不能很好地契合。

二进制

  相同的问题同样发生在二进制世界里面。只不过,问题会更糟糕。因为基数2的质数因子要比基数10少一个5,所以出现循环分数的几率会高很多很多。0.1就是一个例子。

在十进制中,表示0.1很简单,就是0.1。但是在二进制中,它是0.00011001100110011...,这又是一个循环分数!

所在在尝试执行0.1 + 0.2 的计算时,就变成了下面的情况:

 0.0001100110011
+ 0.0011001100110
= 0.0100110011001
= 0.299926758

由于类似四舍五入这样模式的原因,我们最后会得到类似0.300000000004的结果。我不会在这篇文章中这个问题讨论(但在以后的文章中会这样做)。通过引入舍入模式,减少了这一事实导致的错误数量。

精度


  另一个主要问题在精确度,我们只有固定数量的比特位用于存储浮点数的有效数字。

十进制

  举个例子,假如我们只能够存储3位有效数字。那么我们比较 333 和 1/3 * 10^ 3,会发现在我们的系统中它们是完全相同的。这是因为我们只存储了数字的3位有效数字,而且这涉及到循环分数的截断。

再看一个极端的例子中,当我们只有3个十进制位来存储数字的有效数字的话,那么 1 + 1*10^3^ 的结果还是 1*10^3^ ,并不是1001。要想得到1001的结果,必须需要4个十进制位才行。

二进制

  相同的问题也会发生在二进制中非常非常小和非常非常大的数字上。在未来的一篇文章中,我将更多地谈论浮点数的局限性。为了完整性起见,考虑一下前面的二进制例子,我们现在有3个二进制位来表示我们的有效数字。用基数2代替上面例子中的10,那么 1 + 1*2^3^ 的结果也依旧是 1*2^3^。这是因为要表示1001(相当于十进制中的9),需要4个二进制位,较低位的有效数字在转换中会被丢失。

  这里没有解决方案,精度的极限是由我们可以存储的数量来定义的。要解决这个问题,可以使用较大的浮点数据类型。例如采用双精度浮点数取代单精度浮点数。

总结


  综上所述,浮点数是一种类似于标准格式或科学符号的二进制值的表示方法。它允许我们精确地存储很大很大的数和很小很小的数。为了做到这一点,我们将小数点相对于数字的有效数字移动,使该数字以与所使用的基数成比例的速度变大或变小。大多数与浮点相关的错误都是以在有限的位数中表示循环分数的形式出现的。四舍五入模式有助于减少这些错误。

原文地址


What's Up With Floating Point?