1、浮点数的计算精度问题
先进行一个小实验。以python语言为例,打开python的命令行工具ipython,并在其中输入一个带小说的表达式,观察输出的结果。你会发现输出的结果与我们预期的计算结果0有着很大差别。
In [1]: 0.9-0.3*3
Out[1]: 1.1102230246251565e-16
造成这一结果的原因是,我们计算机是二进制的形式来存储数据的,而99%的浮点数都无法用二进制精确表示。因此在上面的例子中输入的几个浮点数,在计算机中都是以近似值的形式存储,用多个近似值进行计算的时候,就出现精度丢失的问题,导致我们看到的结果与预期输出有着很大差异。
在计算机数的概念中,一般用浮点数和定点数来表达实数,而整数和小数是我们在数学课本上的说法。定点数是指小数点固定的数,固定的小数点位置决定数的整数部分和小数部分固定,当我们表示整数时,我们把小数点固定在 最右面,因此整数在计算机中可用定点数表示。然而这种方式比较僵硬,不利于表达过于大或者过于小的数,因此浮点数在这一块上的作用就非常明显。浮点数是相对定点数的概念而来的,小数点的位置不固定,计算机通常无法用二进制的方式精确表达这类数,一般都是科学计数法表达,即一个尾数(Mantissa ),一个基数(Base),一个指数(Exponent)以及一个表示正负的符号来表达实数,利用指数的变化从达到小数点浮动的效果,从而可以灵活地表达更大范围的实数。比如 123.45 用十进制科学计数法可以表达为 1.2345 × 102 ,其中 1.2345 为尾数,10 为基数,2 为指数。其中需要说明的是,尾数有时也称为有效数字(Significand),尾数实际上是有效数字的非正式说法。
2、浮点数的表达方式
2.1 浮点数数学表示
规范的浮点数的表达方式具有以下形式:
$$d_0.d_1d_2…d_n × β^e,(0 ≤ di < β)$$
其中$d_0.d_1d_2…d_n$即尾数,β 为基数,e 为指数。尾数中数字的个数称为精度,在本文中用 p(presion) 来表示。每个数字 d 的取值范围为$[0, d)$,且小数点左侧的数字不为 0,即$d_0$取值不能为0。
将该值表示为具体的值可通过以下的表达式计算得到:(p为计算精度)
$$±(d_0β^0 + d_1β^{-1} + … + d_{p-1}β^{-(p-1)})β^e , (0 ≤ d_i < β)$$
对于常见的十进制科学表示法,这种计算方式容易理解得多,其中β为10,p为精度。计算机内部的数值都是基于二进制表达的,表达式中的d只能在0和1之间取值,基数β为2。例如二进制数1001.101,通过表达式计算
$$1 × 2^3 + 0 × 2^2 + 0 × 2^1 + 1 × 2^0 + 1 × 2^{-1} + 0 × 2^{-2} + 1 × 2^{-3}$$
可得到对应十进制的9.625,而这个二进制小数的规范浮点数表达式为 $1.001101 × 2^3$。
计算机中是用有限的连续字节保存浮点数的,IEEE定义了多种浮点格式,我们常见一般是:单精度、双精度、扩展双精度这三种格式,不同精度的浮点格式适用于不同的计算要求。单精度(Single Precision)浮点数是32位(即4字节)的,双精度(Double Precision)浮点数是64位(即8字节)的。在 IEEE 标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中各个域保存的值分别用于表示给定二进制浮点数中的符号,指数和尾数。具体格式参见下表:
- | 长度 | 符号 | 指数 | 尾数 | 有效位数 | 指数偏移 | 说明 |
---|---|---|---|---|---|---|---|
单精度 | 32位 | 1 | 8 | 23 | 24 | 127 | 有1个隐含位 |
双精度 | 64位 | 1 | 11 | 52 | 53 | 1023 | 有1个隐含位 |
扩展双精度 | 80位 | 1 | 15 | 64 | 64 | 16383 | 没有隐含位 |
- 第一个域:为符号域。其中 0 表示数值为正数,而 1 则表示负数。
- 第二个域为指数域,对应于我们之前介绍的二进制科学计数法中的指数部分。
- 图例中的第三个域为尾数域,其中单精度数为 23位长,双精度数为 52 位长。
对于单精度数而言,上面例子说的二进制的 1001.101(对应于十进制的 9.625)可以表达为$1.001101 × 2^3$,所以实际保存在尾数域中的值为 00110100000000000000000,即去掉小数点左侧的 1,并用 0 在右侧补齐。
根据以上表格的分析,IEEE 754标准中定义浮点数的表示范围为:
精度 | 二进制范围 | 对应的十进制范围 | 备注 |
---|---|---|---|
单精度 | $\pm(2-2^{-23}) × 2^{127}$ | ~ $\pm10^{38.53}$ | 小数点后6-7位 |
双精度 | $± (2-2^{-52}) × 2^{1023}$ | - | 小数点后15-16位 |
浮点数的表示有一定的范围,超出范围时会产生溢出(Flow),一般称大于绝对值最大的数据为上溢(Overflow),小于绝对值最小的数据为下溢(Underflow)。
2.2 浮点数的表示约定
单精度浮点数和双精度浮点数都是用IEEE 754标准定义的,其中有一些特殊约定,例如:
- 当P=0,M=0时,表示0。
- 当P=255,M=0时,表示无穷大,用符号位来确定是正无穷大还是负无穷大。
- 当P=255,M≠0时,表示NaN(Not a Number,不是一个数)。
2.3 浮点数的精度
根据IEEE(美国电气和电子工程师学会)754标准要求,无法精确保存的值必须向最接近的可保存的值进行舍入,这有点像我们熟悉的十进制的四舍五入。很多小数根本无法在二进制计算机中精确表示(比如最简单的 0.1)由于浮点数尾数域的位数是有限的,为此,浮点数的处理办法是持续该过程直到由此得到的尾数足以填满尾数域,之后对多余的位进行舍入。
换句话说,除了我们之前讲到的精度问题之外,十进制到二进制的变换也并不能保证总是精确的,而只能是近似值。事实上,只有很少一部分十进制小数具有精确的二进制浮点数表达。再加上浮点数运算过程中的误差累积,结果是很多我们看来非常简单的十进制运算在计算机上却往往出人意料。这就是最常见的浮点运算的”不准确”问题。