浮点数运算

在电脑科学中，浮点数运算（Floating-point arithmetic）是指以“浮点”（floating point，缩写为 FP）格式来表示实数并进行运算的一种方式。浮点是一种实数的近似数值表示法，由一个有效数字（即尾数）乘以特定基数的整数次指数（幂数）构成。以这种表示法表示的数值称为浮点数（floating-point number）。由于电脑无法精确表示所有实数，浮点数运算通常会伴随着近似值处理或舍入。

电脑采用浮点数运算的主因在于其底层的二进位制运算逻辑。例如：4 ÷ 2 = 2，转换为二进制即 100(2) 变成 010(2)，相当于将数值退一位数。同理，1.0 ÷ 2 = 0.5，二进制表示为 0.1(2)，也就是 $\frac{1}{2}$。以此类推，二进制的 0.01(2) 即为十进制的 $\frac{1}{2^{2}}$ = $\frac{1}{4}$ = 0.25。由于十进位制中许多小数（如 0.1）无法精确转换为有限位数的二进制小数，因此系统只能以近似值的方式来表达。

这种表示法类似于基数为 10 的科学记数法。在电脑上，通常使用 2 为基数，一个浮点数 a 可由尾数 m 与指数 e 表示为：a = m × be。在任意一个这样的系统中，可选择一个基数 b 与精度 p（即使用多少位元来储存）。m（即尾数）是格式如 ±d.ddd...ddd 的 p 位数（每一位数字介于 0 到 b-1 之间，包含 0 与 b-1）。若 m 的首位非 0，则称该浮点数已正规化（Normalized）。实务上通常会使用独立的符号位元（s 代表正或负）来表示正负号，此时 m 预设为正值。

这种设计基于对数值“表示范围”与“精密度”之间的取舍：可以在某个固定长度的储存空间内表示出某个实数的近似值。例如：一个指数范围为 ±4 的 4 位十进制浮点数可以表示 43210、4.321 或 0.0004321，但没有足够的精确度来表示 432.123 和 43212.3（必须舍入近似为 432.1 和 43210）。当然，现代电脑实际使用的位数通常远大于 4。

此外，浮点数表示法通常还包含一些特殊数值：+∞ 和 −∞（正负无穷大）以及 NaN（“Not a Number”，非数）。无穷大用于表示数值大到无法表示的状况，NaN 则用来指示非法操作或无法定义的运算结果。

在内存中，无穷大（inf）的阶码（指数）为全 1，尾数为全 0；而 NaN 的阶码为全 1，尾数则不全为 0。

## 电脑的浮点数

浮点指的是带有小数的数值，浮点运算即是小数的四则运算，常被用来测量电脑的运算速度。大部分电脑采用二进制（b=2）的表示方法。位元（bit）是衡量浮点数所需储存空间的单位，通常为 32 位元或 64 位元，分别称为单精度和双精度。部分系统提供更大的浮点数，例如英特尔的浮点运算单元 Intel 8087协处理器（及其后续整合进 x86 处理器的产品）提供了 80 位元长的浮点数，用于储存浮点运算的最后或中间结果。此外，也有一些系统提供 128 位元的浮点数（通常以软件实现）。

## 浮点数的标准

电脑中使用的浮点数标准已被电气电子工程师协会（IEEE）规范化为 IEEE 754。

## 举例

π 的值可以表示为 π = 3.1415926...10（十进制）。当在一个支援 17 位尾数的电脑系统中表示时，它会近似为 0.11001001000011111 × 22。

## 浮点数运算

为了方便阅读与呈现，以下的例子将采用十进制、有效位数 7 位数的浮点数（类似 IEEE 754 decimal32 格式），其底层原理不会随进位制或有效位数而改变。此处的 s 表示尾数（有效数字），e 表示指数。

### 加减法

处理浮点数加法的简单作法是将两个浮点数调整至具有相同的指数。在以下例子中，第二个数的小数点左移了三位，使两者的指数相同，随后即可进行一般的加法运算：

```
  123456.7 = 1.234567 × 10^5
  101.7654 = 1.017654 × 10^2 = 0.001017654 × 10^5

  因此
  123456.7 + 101.7654 = (1.234567 × 10^5) + (1.017654 × 10^2)
                      = (1.234567 × 10^5) + (0.001017654 × 10^5)
                      = (1.234567 + 0.001017654) × 10^5
                      =  1.235584654 × 10^5
```

若用 e 和 s 来表示：

```
  e=5;  s=1.234567     (123456.7)
+ e=2;  s=1.017654     (101.7654)

  e=5;  s=1.234567
+ e=5;  s=0.001017654  (對齊移位後)

  e=5;  s=1.235584654  (實際的和：123558.4654)
```

这是未经修约的真实结果。随后系统会将其四舍五入到 7 位有效数字，并在需要时进行正规化，最终结果为：

```
  e=5;  s=1.235585    (最後答案：123558.5)
```

加数的最低三位数（654）在结果中被丢弃了，这种现象称为舍入误差。在一些极端的例子中，两个浮点数相加的和，可能会与其中较大的一方完全相等：

```
  e=5;  s=1.234567
+ e=−3; s=9.876543

  e=5;  s=1.234567
+ e=5;  s=0.00000009876543 (對齊移位後)

  e=5;  s=1.23456709876543 (真實的和)
  e=5;  s=1.234567         (四捨五入及正規化後)
```

在上述例子中，当两数指数差距极大时，需要极高的精度才能获得准确的结果。不过在二进制的加减法硬件设计中，只要利用保护位元（Guard bit）、舍入位元（Rounding bit）以及额外的黏滞位元（Sticky bit），即可确保舍入结果的正确性。

另一种严重失去有效数字的情形发生在两个几乎相等的数字相减时。在以下例子中，e = 5; s = 1.234571 和 e = 5; s = 1.234567 分别是有理数 123457.1467 和 123456.659 的近似值：

```
  e=5;  s=1.234571
− e=5;  s=1.234567

  e=5;  s=0.000004
  e=−1; s=4.000000 (四捨五入及正規化後)
```

如同Sterbenz 引理所证明的，即使因为渐进式下溢而出现下溢，浮点数的差仍可被精确计算。然而，原始两数的真实差值约为 e = −1; s = 4.877000，这与浮点数运算得到的 e = −1; s = 4.000000 之间存在超过 20% 的误差。在极端情况下，甚至所有的有效数字都可能完全消失。这种称为灾难性抵消的现象显示，若草率假设计算结果的每一位数字都是可靠的，将面临极大风险。这类误差的处理与修正是数值分析领域的核心课题之一。

### 乘除法

若要进行乘法，将有效数字相乘，指数相加，再进行四舍五入及正规化即可：

```
  e=3;  s=4.734612
× e=5;  s=5.417242

  e=8;  s=25.648538980104 (真實乘積)
  e=8;  s=25.64854        (四捨五入後)
  e=9;  s=2.564854        (正規化)
```

除法则会将被除数与除数的有效数字相除，两者的指数相减，接着再进行四舍五入及正规化。

乘除法运算通常不会面临灾难性抵消或大数吃小数的问题，但依然会产生舍入误差。如果进行连续运算，这些误差可能会逐步累积放大。实务上，要在硬件层面实现这类运算的数位逻辑相当复杂（例如布斯乘法算法与除法器设计）。

## 准确性

由于浮点数无法完美表达所有实数，浮点运算的结果与理论上的数学运算会有所差异，有时此差异甚至极为显著。

例如，二进制浮点数无法精确表达 0.1 和 0.01。0.1 的平方既不是精确的 0.01，也不会是最接近 0.01 的可表达数值。单精度（24 位元有效数字）浮点数表示 0.1 的结果为 $e=-4$, $s=110011001100110011001101_{(2)}$，其十进制真值为：

0.100000001490116119384765625

此数值的平方为：

0.010000000298023226097399174250313080847263336181640625

然而，在该系统中最接近 0.01 的可表达数值实际上是：

0.009999999776482582092285156250

此外，浮点数也无法精确表达圆周率 $\pi$，因此在电脑上计算 $\tan\frac{\pi}{2}$ 并不会得到正无穷大，也不会触发溢位。以下方的 C 语言程式码为例：

```c
double pi = 3.1415926535897932384626433832795;
double z = tan(pi/2.0);
```

该计算结果为 16331239353195370.0；若改用单精度浮点数计算，结果则为 −22877332.0。同理，$\sin \pi \neq 0$。

由于浮点数计算过程中不可避免地会流失精度，浮点运算的代数性质与纯数学运算并不相同，例如浮点数的加法与乘法皆不符合结合律与分配律。

### 历史事故

早期 P5 世代的 60-100MHz 奔腾处理器在浮点运算单元（FPU）存在瑕疵。在极少数情况下，会导致除法运算的精确度大幅降低。这个硬件缺陷于 1994 年被发现，成为广为人知的奔腾浮点除错误（Pentium FDIV bug）。此事件让英特尔面临巨大的公关危机，最终不得不启动全面回收计划来更换有缺陷的处理器。