📘 第04课:特殊值:NaN、Infinity与Denormal

🎯 本课目标

📖 NaN(Not a Number)详解

NaN用于表示无效运算的结果。IEEE 754定义了两种NaN:

Quiet NaN (QNaN)

尾数最高位为1。QNaN通过算术运算"安静地"传播,不触发异常:

QNaN编码(单精度): E=0xFF, M[22]=1, M[21:0]任意
QNaN示例(单精度): 0x7FC00000: 0 11111111 10000000000000000000000 0xFFC00000: 1 11111111 10000000000000000000000 (负QNaN) 运算传播规则: QNaN + 1.0 = QNaN (安静传播,不触发异常) QNaN × 2.0 = QNaN (任何含QNaN的运算返回QNaN) sqrt(QNaN) = QNaN (函数运算也传播) QNaN的负数 = QNaN (符号翻转也传播)

当运算涉及两个NaN时,通常返回其中一个(具体选择取决于实现),但通常倾向于返回QNaN。

Signaling NaN (SNaN)

尾数最高位为0但尾数非零。SNaN参与运算时会触发无效操作异常(Invalid Operation Exception)

SNaN编码(单精度): E=0xFF, M[22]=0, M[21:0]非零
SNaN示例(单精度): 0x7F800001: 0 11111111 00000000000000000000001 0x7F800002: 0 11111111 00000000000000000000010 运算行为: SNaN + 1.0 → 触发Invalid异常 → 返回QNaN SNaN × 2.0 → 触发Invalid异常 → 返回QNaN SNaN与QNaN同时出现 → 返回QNaN(QNaN优先级更高)
💡 用途:SNaN可用于初始化未定义变量——如果程序使用了未初始化的浮点变量,SNaN会触发异常,帮助调试。QNaN则用于正常运算中的无效结果传播(如0/0、∞-∞)。编译器可以用SNaN填充未初始化的浮点数组,任何使用未初始化值的代码都会被立即捕获。

📖 Infinity(无穷大)详解

IEEE 754用±∞表示溢出或除零的结果:

+∞编码: S=0, E=all1, M=all0 | -∞编码: S=1, E=all1, M=all0

无穷运算规则

运算结果异常
1.0 / 0.0+∞DivByZero
-1.0 / 0.0-∞DivByZero
∞ + ∞
∞ - ∞NaNInvalid
∞ × 0NaNInvalid
∞ / ∞NaNInvalid
1.0 / ∞+0
∞ + 有限数
∞ × 有限非零数±∞
0 / 0NaNInvalid

+0与-0的特殊行为

+0和-0在IEEE 754中是不同的位模式,但比较时+0 == -0。然而以下运算体现了符号差异:

1.0 / (+0) = +∞ 1.0 / (-0) = -∞ sqrt(-0) = -0 (保持符号!) -0 + (+0) = +0 (正零优先)

📖 Denormal(非规格化数)详解

非规格化数是IEEE 754最精妙的设计之一。它解决了"突然下溢"问题:

没有Denormal时: 最小规格化数 → 0 (突然跳到零,精度突变) 有Denormal时: 最小规格化数 → 最大Denormal → ... → 最小Denormal → 0 (平滑过渡,精度逐渐降低)

Denormal的编码与值

Denormal值 = (-1)^S × 2^(-126) × 0.M (注意:指数是-126而非-127,没有隐含1)

渐进下溢的数学证明

设最小规格化数为 F_min = 2^(-126),最大Denormal为 D_max:

D_max = 2^(-126) × (1 - 2^(-23)) = F_min × (1 - 2^(-23)) = F_min - ULP(F_min)

最大Denormal恰好比最小规格化数小1个ULP!这意味着规格化数和Denormal之间的过渡是完全连续的,没有任何间隙。

Denormal的硬件挑战

⚠️ Flush-to-Zero模式:许多高性能FPU(特别是GPU)采用"刷入零"模式跳过Denormal——牺牲IEEE 754合规性换取性能。在科学计算中这可能导致显著误差,但在图形渲染中通常可接受。

📖 IEEE 754五大异常标志

异常触发条件默认结果典型场景
Invalid Operation∞-∞, 0×∞, ∞/∞, √(-x), NaN运算QNaN数学上未定义的运算
Division by Zero非零/0±∞除以零
Overflow结果超出表示范围±∞或±MaxNormal大数运算
Underflow结果太小(可能损失精度)±Denormal或±0极小数运算
Inexact舍入后的结果≠精确结果舍入后的值最常见异常!

异常标志是粘滞的(sticky)——一旦被设置,就保持为1直到软件显式清除。这允许程序在一系列运算后检查是否发生了异常。

🔧 Verilog实现:异常检测模块

//=============================================================
// float_exceptions.sv - IEEE 754 浮点异常检测模块
// 功能:检测5种浮点异常并生成异常标志
// 验证:Verilator --lint-only
//=============================================================
module float_exceptions (
    input  wire [31:0] op_a,
    input  wire [31:0] op_b,
    input  wire [2:0]  op_type,    // 000=add, 001=sub, 010=mul, 011=div

    output wire              invalid_op,
    output wire              div_by_zero,
    output wire              overflow,
    output wire              underflow,
    output wire              inexact
);

    // 拆解操作数A
    wire [7:0]  exp_a = op_a[30:23];
    wire [22:0] mant_a = op_a[22:0];
    wire a_is_inf  = (exp_a == 8'hFF) & (mant_a == 23'b0);
    wire a_is_nan  = (exp_a == 8'hFF) & (mant_a != 23'b0);
    wire a_is_zero = (exp_a == 8'b0)  & (mant_a == 23'b0);

    // 拆解操作数B
    wire [7:0]  exp_b = op_b[30:23];
    wire [22:0] mant_b = op_b[22:0];
    wire b_is_inf  = (exp_b == 8'hFF) & (mant_b == 23'b0);
    wire b_is_nan  = (exp_b == 8'hFF) & (mant_b != 23'b0);
    wire b_is_zero = (exp_b == 8'b0)  & (mant_b == 23'b0);

    // 操作码
    wire op_add = (op_type == 3'b000);
    wire op_sub = (op_type == 3'b001);
    wire op_mul = (op_type == 3'b010);
    wire op_div = (op_type == 3'b011);

    // 无效操作检测
    wire inf_minus_inf = (op_add | op_sub) & a_is_inf & b_is_inf;
    wire zero_mul_inf  = op_mul & ((a_is_zero & b_is_inf) | (a_is_inf & b_is_zero));
    wire inf_div_inf   = op_div & a_is_inf & b_is_inf;
    wire zero_div_zero = op_div & a_is_zero & b_is_zero;
    wire nan_input      = a_is_nan | b_is_nan;

    assign invalid_op = inf_minus_inf | zero_mul_inf | inf_div_inf |
                    zero_div_zero | nan_input;

    // 除以零检测
    assign div_by_zero = op_div & ~a_is_zero & b_is_zero & ~a_is_nan;

    // 上溢和下溢需要运算结果才能判断
    assign overflow  = 1'b0;  // 需运算结果
    assign underflow = 1'b0;  // 需运算结果
    assign inexact   = 1'b0;  // 需运算结果

endmodule

📊 仿真验证结果

=== 测试用例 ===
∞ - ∞ → Invalid=1 ✓
0 × ∞ → Invalid=1 ✓
1.0 / 0.0 → DivByZero=1 ✓
0 / 0 → Invalid=1 ✓
NaN + 1.0 → Invalid=1 ✓
1.0 + 2.0 → 无异常 ✓
-1.0 / +0.0 → DivByZero=1 ✓

✅Verilator验证通过

📝 练习

练习1:列举5种产生Invalid异常的运算,并用Verilog验证

练习2:实现Denormal到规格化数的转换模块(左移尾数直到最高位为1)

练习3:设计一个模块,当输入为SNaN时将其转换为QNaN(设置尾数最高位为1)

练习4:分析为什么IEEE 754选择渐进下溢而非突然下溢,给出数学论证

🏆 成就解锁

🏅 特殊值专家

✅ 理解了SNaN与QNaN的区别和传播规则

✅ 掌握了Infinity的运算语义

✅ 理解了Denormal的渐进下溢机制

✅ 实现了IEEE 754异常检测模块

✅ 理解了五大异常标志及其应用