4. 浮点运算与精度测试

数值算法的正确性验证绕不开一个前提:计算机里的实数并不是数学意义上的实数。任何基于浮点的算法测试,如果忽略表示误差与舍入行为,就可能把“算法正确但结果有微小偏差”误判为失败,也可能把“误差已累积到不可接受”误判为通过。本节围绕 IEEE 754 的关键约定、舍入模式、机器精度与单位舍入误差、浮点比较策略,以及如何据此设计精度测试用例展开。

一、IEEE 754 的关键要点

IEEE 754 用“符号位 + 指数 + 尾数”的二进制格式表示浮点数。以最常见的双精度 binary64 为例:1 位符号、11 位偏置指数(偏置量 1023)、52 位尾数。规格化数的值为 (−1)s × 1.f × 2e−1023,其中隐含的前导 1 不存储,因此有效精度为 53 位二进制位。单精度 binary32 对应 8 位指数(偏置 127)与 23 位尾数,有效精度 24 位。

测试中必须关注的几个特殊值:正负零(比较相等但符号不同)、正负无穷(溢出或除零产生)、NaN(无效运算产生,且 NaN ≠ NaN)。此外还有次正规数(指数全 0、无隐含前导 1),它让 0 与最小规格化数之间的间隙被填满,但精度随数值减小而下降。很多精度缺陷恰恰出现在次正规数区间或接近溢出边界处,因此边界值用例不能只取“普通大小”的数。

二、舍入模式及其对结果的影响

IEEE 754 定义了多种舍入方向:就近舍入到偶数(roundTiesToEven,默认)、就近舍入远离零、向正无穷、向负无穷、向零。默认的“就近偶数”在恰好居中时选择末位为偶数的候选值,这避免了统计上的系统性偏置。测试时若算法对舍入方向敏感(如区间运算、累加求和、定点模拟),应显式设置舍入模式并在该模式下重跑用例;同一输入在不同舍入模式下允许出现末位差异,但差异幅度应可解释。

浮点精度测试的核心要素与判定链 表示层(IEEE 754) 符号 / 指数 / 尾数 binary64:53 位有效精度 特殊值:±0、±∞、NaN 次正规数:精度递减区 误差层 机器精度 ε ≈ 2.22e−16 单位舍入误差 u = ε/2 绝对误差 / 相对误差 舍入模式影响末位 判定层 容差:atol + rtol·|期望| NaN 需显式判定 边界与次正规数用例 通过 / 失败 / 待复核 精度测试用例设计维度 典型值 常规量级、正负混合 边界值 最小规格化数、溢出 病态输入 大数吃小数、相减抵消 特殊值 NaN、±∞、±0、次正规

三、机器精度与单位舍入误差

机器精度(machine epsilon)常定义为 1 与“大于 1 的最小可表示浮点数”之差,双精度下 ε = 2−52 ≈ 2.220446049250313×10−16。而单位舍入误差 u 通常取 ε/2 ≈ 1.11×10−16,它刻画了单次正确舍入运算的相对误差上界。二者在文献中定义略有差异,测试文档中应写明采用哪一种,避免阈值口径不一致。

一个可直接执行的验证方法:从 1.0 开始反复折半,直到 1.0 + h 不再大于 1.0,此时最后一个使和仍大于 1.0 的 h 即为 ε 的估计值。用 Python 可写为:

h = 1.0
while 1.0 + h / 2.0 > 1.0:
    h /= 2.0
print(h)          # 约 2.220446049250313e-16
print(h / 2)      # 单位舍入误差约 1.11e-16

这个用例本身就是一个“精度自检”用例:若某平台输出明显偏离该量级,说明编译选项(如快速数学、x87 扩展精度)或数据类型选择存在问题。注意在 x87 80 位扩展精度寄存器参与运算时,中间结果精度高于存储精度,可能导致该循环行为与预期不同,测试时应强制中间结果落回 64 位。

四、浮点比较与容差设定

浮点结果不应使用 == 直接比较。工程上常用组合容差:当 |实际 − 期望| ≤ atol + rtol × |期望| 时判为通过。atol 用于兜住接近零的绝对误差,rtol 用于随量级放大的相对误差。容差不是越小越好:若 rtol 小于算法本身的误差累积量级,正确实现也会被判失败;若 rtol 过大,则掩盖真实缺陷。合理做法是先由算法误差分析给出理论界,再取略宽于该界的值作为测试阈值,并在用例中注明依据。

还需处理三类特殊情况:其一,期望值为 0 时 rtol 项失效,必须依赖 atol;其二,NaN 参与比较时任何大小判断都为假,需先用 isnan 显式判定,再决定该用例应“期望产生 NaN”还是“禁止产生 NaN”;其三,无穷大应比较符号与类别,而非数值大小。下面是一个可复用的比较函数示例:

import math

def close(actual, expected, atol=1e-12, rtol=1e-9):
    if math.isnan(expected):
        return math.isnan(actual)
    if math.isinf(expected):
        return actual == expected
    return abs(actual - expected) <= atol + rtol * abs(expected)

五、精度相关测试用例的设计

精度用例应覆盖四个维度。典型值验证算法在常规量级下是否给出符合容差的结果;边界值验证最小规格化数附近、最大有限值附近以及溢出/下溢行为;病态输入验证算法在“大数吃小数”“相近数相减抵消”“长链累加”等情形下的稳定性;特殊值验证 NaN、±∞、±0 的传播规则是否符合预期。

以“求和算法”为例,可设计如下用例:输入 [1e16, 1.0, −1e16],朴素顺序求和得到 0.0,而数学期望为 1.0,该用例用于暴露累加顺序导致的精度损失;若被测算法采用 Kahan 补偿求和,则应通过。再如计算 √(x+1) − √x 在 x = 1e16 时,直接相减会因抵消丢失大量有效位,测试可对比直接公式与有理化变形 (1)/(√(x+1)+√x) 的结果差异,并据此判断实现是否采用了数值稳定形式。

设计容差时建议同时记录“绝对误差”和“相对误差”两个观测值,失败用例中哪一个超限往往直接指向问题类型:绝对误差超限多见于接近零的结果,相对误差超限多见于量级放大后的累积误差。
不要用“打印结果看起来一致”代替断言。十进制打印默认只保留有限位,可能掩盖末位差异;测试应以二进制比较或显式容差断言为准,并在报告中输出实际误差数值。

最后,精度测试的结论应可复现:用例中固定输入数据、舍入模式、编译选项与容差参数,输出实际值、期望值、绝对误差、相对误差与判定结果。只有把“表示—误差—判定”这条链上的每个环节都显式化,浮点精度测试才具备工程可信度,而不是依赖偶然通过的运气。