谁动了我的精度 — 浮点数运算的问题
目录
1 引子
1 2 3 4 |
// ... // float a = 0.2, b=0.4; if(a + b == 0.6) // ... |
这个 if 表达式被判定为 false, 程序没有按照预订执行下去。0.2 + 0.4 = 0.6,这还会有错吗?同事表示其小学数学还是很过关的。 是的,在人类的认知里,这毫无疑问是正确的,但是在计算机的认知里,就不一定了。这需要我们了解浮点数这种数据类型。
2 浮点数
2.1 用二进制表示小数
首先我们来想一下如何来表示一个十进制整数 d:
$$ d_m d_{m-1} ... d_1 d_0 d_{-1} d_{-2} ... d_{-1}, d\in [0,9] $$
这个表示方法描述的数值 d 的定义如下:
$$ d = \sum_{i=-1}^m{10^i \times d_i} $$
同样,引申到小数,小数点左边的数字是 10 的非负幂,得到整数部分;小数点右面的数字是 10 的负幂,得到小数部分。
例如: \(12.34_{10}\) 所表示的字为: \( 1 \times 10^{1} + 2 \times 10^{0} + 3 \times 10^{-1}
+ 4 \times 10^{-2} =12 \frac{34}{100} \)
类似地我们考虑一个二进制数 b 的表示:
$$ b_{m} b_{m-1} ... b_{1} b_{0} b_{-1} ... b_{-n} ,b\in[0,1] $$
它的定义如下:
$$ b = \sum_{i=-n}^{m}2^i \times d_i $$
如 \(10.11_2\) 表示数字: \(1 \times 2^1 + 1 \times 2^{0} + 1 \times 2^{-1} + 1 \times 2^{-2} = 2 \frac{3}{4} \)
假定在二进制中,我们的编码长度有限,我们就不能,或者不能精确的表示某一些数字。假定我们的编码长度只有8位,那么我们可以准确表达的数有 28 = 256 个。如果用来表示无符号整数,范围为 0~255,如果表示有符号整数,范围为 -128~127.如果用来表示小数呢?由前面我们可以知道,小数的二进制表示法只可以表示能写成 \(b \times 2^{i}\) 的娄数,如
0.12 = 1 x (1/2) = 0.510,
0.112 = 1 x (1/2) + 1 x (1/4) = 0.7510
其它的值都只能被近似的表示。我们在十进制中可以轻松表达的数,如 0.20, 0.30 都无法在有限的二进制中精确的表达。就如我们在十进制中无法精确地表达某些数,如 \( \frac{1}{3},\frac{5}{7} \) 一样, .对于这些数,增加编码的长度只能提高数的精度。
如果一个十进制小数可以使用二进制精确表达,那么它的最后一个数字一定是 5 。正在阅读这篇文章的你,可以试着证明一下。 (这是一个必要不充分语句)
2.2 IEEE 浮点数
2.2.1 IEEE 标准
IEEE 浮点标准使用公式:
$$ V = (-1)^{s} \times M \times 2^{E} $$
来表示一个浮点数。其中:
- s 符号位,决定该数是正(s=0)还是负(s=1).
- M 尾数,是一个二进制小数,取值获取为 1 ~ \( 2-\epsilon\)
- E 阶码,作用是对浮点数加权,权重是2的E次幂
在C语言中,对于一个浮点占用32位内存(双精度占用64位),这块内存被划分为三个区:
- 最高位为单独的符号区, s = 1 位
- 接下来 k = 8 位为阶码编码区。值用 \(e\) 表示
- 余下来 n = 23 位为尾数编码区。值用 \(f\) 表示
根据阶码编码区的值,将浮点数的表示分为三种情况:规格化、非规格化与特殊值
2.2.2 规格化浮点数
这是最常见的一种情况,阶码区里即不全是0,也不全是1. 即 \(e \not= 0\) 且 \(e \not= 255\) 在这种情况下,阶码字段被解释为 偏置 的有符号整数。阶码的值为: $$ E = e - Bias , Bias = 2^{(k-1)}-1 $$ 那么这里的 Bias 为 28-1-1 = 127, \(e\) 的取值范围为 1 ~ 254,那么 E 的取值范围为 -126 ~ 127 。
而对于尾数部分,将其解释为小数值 \( f, 0 \leq f \lt 1\) .而尾数定义为: $$ M = 1 + f $$
2.2.3 非规格化浮点数
当阶码全为 0 是,即 \(e=0\) 时,表表示的数值即为非规格化形式的。在这种情况下,码的值: $$ E= 1-Bias $$ 尾数的值为: $$ M=f $$ 因为使用规格化数时,我们必须总令 \(M \geq 1\),所以我们无法表示 0 值。那么在非规格化形式中,当符号位为0,阶码值为0,尾数区为0时,我们得到 +0.0,而当符号位为1时,我们得到 -0.0
2.2.4 特殊值
当阶码区全为1,即 \(e=255\)时,表示特殊值。
当尾数区全为0时,得到的值表示无穷:当 s=0时,表示 \(+\infty\),当s=1 时,表示\(-\infty\)
当尾数区不全为0时,得到的值表示 'Nan' (Not a Number).
2.2.5 示例
为了便于描述,我们假设某种语言的float数据类型占用6位内存。最高位表示符号号,次高4位为阶码区,最后3位为尾数区。那么有 \(k=4,Bias=2^{k-1}-1=7\).我们来看一下:
描述 | 位表示 | 指数 | 小数 | 值\((-1)^{s}\times M\times 2^E\) |
---|---|---|---|---|
0 | 0 0000 000 | \(e=0, E=-6\) | \( f=0,M=f=0\) | \( M \times 2^{E} = 0\) |
非规格化数 | 0 0000 001 | \(e=0,E=-6\) | \(f=\frac{1}{2^3},M=f=\frac{1}{8}\) | \( M\times 2^{-6}=\frac{1}{512}=0.001953\) |
… | ||||
非规格化数 | 0 0000 111 |
|
\(f=\frac{7}{2^3},M=f=\frac{7}{8}\) | \( M \times 2^{-6}=\frac{7}{512}=0.013672\) |
规格化数 | 0 0001 000 | \(e=1,E=-6\) | \(f=0,M=1+f=1\) | \( 1\times 2^{-6}=\frac{1}{64}=0.015625\) |
… | ||||
规格化数 | 0 1110 111 | \(e=14,E=7\) | \(f=\frac{7}{8},M=1+f=\frac{15}{8}\) | \(\frac{15}{8} \times 2^7=240.0\) |
无穷 | 0 1111 000 | -- | -- | \(\infty\) |
2.2.6 舍入
既然某些十进制小数无法在二进制中精确地表示,那么当我们向计算机中输入一个浮点数,如 0.2 以后,计算机应该怎么存储它呢. IEEE给出的解决方案是,存储它的近似值。IEEE 有一系列舍入运算的标准,如向0舍入、向偶数舍入、向上舍入、向下舍入,以取到最接近要表达的浮点数的数值。具体的硬件或软件厂商在实现这一标准各有不同。
学习使人进步,到此拜读!