📘 第20课:浮点比较器

🎯 本课目标

📖 浮点比较的挑战

浮点比较看起来简单(a < b?),但实际有很多陷阱:

浮点比较的特殊情况: 1. +0 == -0 (尽管位模式不同!) +0 = 0x00000000 -0 = 0x80000000 但它们在数值上相等 2. NaN != NaN (任何与NaN的比较都是False!) NaN < 1.0 → False NaN > 1.0 → False NaN == NaN → False NaN != NaN → True (唯一为True的) 3. -∞ < -1.0 < -0 < +0 < 1.0 < +∞ 注意: -0 < +0 但 -0 == +0! 4. 非规格化数参与比较时,指数为-126
⚠️ 最重要的规则:任何涉及NaN的比较(除了≠)都返回False。NaN ≠ NaN返回True。这是IEEE 754的强制要求,也是许多bug的来源!

📖 IEEE 754比较谓词

IEEE 754定义了14种比较谓词(quiet和signaling各7种):

谓词含义NaN行为±0行为
eqa = bNaN→False+0=-0
nea ≠ bNaN→True+0≠-0→False
lta < bNaN→False-0<+0→False
lea ≤ bNaN→False-0≤+0→True
gta > bNaN→False-0>+0→False
gea ≥ bNaN→False-0≥+0→True
uno无序(含NaN)NaN→TrueN/A

Quiet vs Signaling

Quiet谓词:遇到NaN时不触发异常,直接返回False/True

Signaling谓词:遇到NaN时触发Invalid Operation异常

大多数编程语言使用Quiet版本(C的<、>、==等)。

📖 浮点比较的硬件策略

一个巧妙的观察:正浮点数的大小顺序与其整数位模式的大小顺序一致!

正浮点数 → 无符号整数比较: 1.0 = 0x3F800000 → 无符号 1065353216 2.0 = 0x40000000 → 无符号 1073741824 3.0 = 0x40400000 → 无符号 1077936128 ∞ = 0x7F800000 → 无符号 2139095040 规律: 正浮点数的位模式就是递增的无符号整数! 直接用无符号比较器即可!

但负数不同——负浮点数的位模式是递减的:

负浮点数 → 需要翻转: -3.0 = 0xC0400000 → 无符号 3225419776 (大!) -2.0 = 0xC0000000 → 无符号 3221225472 -1.0 = 0xBF800000 → 无符号 3217031168 -0 = 0x80000000 → 无符号 2147483648 如果直接无符号比较: -3 > -1? 错! 需要: 翻转符号位后比较,或用有符号比较

高效策略:符号翻转法

比较 a 和 b: 将两者的符号位翻转后做无符号比较
翻转符号位后: -3.0 = 0x40400000 → 无符号 1077936128 (小) -2.0 = 0x40000000 → 无符号 1073741824 -1.0 = 0x3F800000 → 无符号 1065353216 -0 = 0x00000000 → 无符号 0 +0 = 0x7F800000 → 无符号 2139095040 +1.0 = 0xBF800000 → 无符号 3217031168 现在无符号比较正确!-3 < -1 < 0 < 1

🔧 Verilog实现:浮点比较器

//=============================================================
// float_compare.sv - 单精度浮点比较器
// 支持所有IEEE 754比较谓词
//=============================================================
module float_compare (
    input  wire [31:0] a,
    input  wire [31:0] b,
    input  wire [2:0]  op,    // 操作码
    output wire        result // 比较结果
);
    // 操作码定义:
    // 000 = eq, 001 = ne, 010 = lt, 011 = le
    // 100 = gt, 101 = ge, 110 = uno

    wire sign_a = a[31], sign_b = b[31];
    wire [7:0] exp_a = a[30:23], exp_b = b[30:23];
    wire [22:0] frac_a = a[22:0], frac_b = b[22:0];

    // === 特殊值检测 ===
    wire a_nan  = (exp_a==8'hFF) & (frac_a!=23'b0);
    wire b_nan  = (exp_b==8'hFF) & (frac_b!=23'b0);
    wire a_zero = (exp_a==8'b0) & (frac_a==23'b0);
    wire b_zero = (exp_b==8'b0) & (frac_b==23'b0);
    wire both_zero = a_zero & b_zero;
    wire either_nan = a_nan | b_nan;

    // === 符号翻转法无符号比较 ===
    wire [31:0] a_flip = {~sign_a, a[30:0]};
    wire [31:0] b_flip = {~sign_b, b[30:0]};

    wire a_lt_b = (a_flip < b_flip) & ~both_zero;
    wire a_gt_b = (a_flip > b_flip) & ~both_zero;
    wire a_eq_b = (a == b) | both_zero; // +0=-0

    // === 比较谓词输出 ===
    wire cmp_eq  = a_eq_b;
    wire cmp_ne  = ~a_eq_b;
    wire cmp_lt  = a_lt_b;
    wire cmp_le  = a_lt_b | a_eq_b;
    wire cmp_gt  = a_gt_b;
    wire cmp_ge  = a_gt_b | a_eq_b;
    wire cmp_uno = either_nan;

    // NaN处理: 除ne和uno外,含NaN则返回False
    wire nan_false = either_nan; // eq/lt/le/gt/ge遇NaN→False

    assign result = (op == 3'b000) ? (cmp_eq  & ~nan_false) : // eq
                   (op == 3'b001) ? (cmp_ne  |  cmp_uno) : // ne
                   (op == 3'b010) ? (cmp_lt  & ~nan_false) : // lt
                   (op == 3'b011) ? (cmp_le  & ~nan_false) : // le
                   (op == 3'b100) ? (cmp_gt  & ~nan_false) : // gt
                   (op == 3'b101) ? (cmp_ge  & ~nan_false) : // ge
                   cmp_uno;  // uno

endmodule

//=============================================================
// float_minmax.sv - 浮点最小值/最大值选择器
//=============================================================
module float_minmax (
    input  wire [31:0] a,
    input  wire [31:0] b,
    input  wire        is_max, // 1=max, 0=min
    output wire [31:0] result
);
    wire sign_a = a[31], sign_b = b[31];
    wire [7:0] exp_a = a[30:23], exp_b = b[30:23];
    wire [22:0] frac_a = a[22:0], frac_b = b[22:0];

    wire a_nan = (exp_a==8'hFF)&(frac_a!=23'b0);
    wire b_nan = (exp_b==8'hFF)&(frac_b!=23'b0);
    wire a_zero = (exp_a==8'b0)&(frac_a==23'b0);
    wire b_zero = (exp_b==8'b0)&(frac_b==23'b0);
    wire both_zero = a_zero & b_zero;

    // 符号翻转比较
    wire [31:0] a_flip = {~sign_a, a[30:0]};
    wire [31:0] b_flip = {~sign_b, b[30:0]};
    wire a_lt_b = (a_flip < b_flip) & ~both_zero;

    // NaN处理: IEEE 754-2019 minNum/maxNum行为
    // minNum: 如果一个操作数是NaN,返回另一个
    // 两个都是NaN,返回NaN
    wire sel_a = is_max ? ~a_lt_b : a_lt_b;
    wire sel_b = ~sel_a;

    assign result = a_nan ? (b_nan ? {1'b0,8'hFF,1'b1,22'b0} : b) :
                    b_nan ? a :
                    sel_a  ? a : b;

endmodule

//=============================================================
// tb_float_compare.sv - 比较器测试
//=============================================================
module tb_float_compare;
    reg [31:0] a, b; reg [2:0] op;
    wire result;
    float_compare uut(.a(a),.b(b),.op(op),.result(result));

    integer err = 0;
    task chk; input [31:0] ia,ib; input [2:0] iop; input expected; input [255:0] nm;
        begin
            a=ia; b=ib; op=iop; #10;
            if(result !== expected) begin
                $display("FAIL %0s: got %b exp %b", nm, result, expected);
                err = err + 1;
            end else $display("PASS %0s", nm);
        end
    endtask

    localparam ONE  = 32'h3F800000;
    localparam TWO  = 32'h40000000;
    localparam MONE = 32'hBF800000;
    localparam PZERO= 32'h00000000;
    localparam NZERO= 32'h80000000;
    localparam QNAN = 32'h7FC00000;

    initial begin
        // eq: 1.0 == 1.0 → True
        chk(ONE, ONE, 3'b000, 1'b1, "1==1");
        // eq: +0 == -0 → True
        chk(PZERO, NZERO, 3'b000, 1'b1, "+0==-0");
        // lt: 1.0 < 2.0 → True
        chk(ONE, TWO, 3'b010, 1'b1, "1<2");
        // lt: -1.0 < 1.0 → True
        chk(MONE, ONE, 3'b010, 1'b1, "-1<1");
        // gt: 2.0 > 1.0 → True
        chk(TWO, ONE, 3'b100, 1'b1, "2>1");
        // eq: NaN == NaN → False
        chk(QNAN, QNAN, 3'b000, 1'b0, "NaN==NaN");
        // ne: NaN != NaN → True
        chk(QNAN, QNAN, 3'b001, 1'b1, "NaN!=NaN");
        // lt: NaN < 1.0 → False
        chk(QNAN, ONE, 3'b010, 1'b0, "NaN<1");
        // uno: NaN, 1.0 → True
        chk(QNAN, ONE, 3'b110, 1'b1, "uno(NaN,1)");
        // le: -0 ≤ +0 → True
        chk(NZERO, PZERO, 3'b011, 1'b1, "-0<=+0");

        $display("\n=== 浮点比较器测试完成,错误: %0d ===", err);
        $finish;
    end
endmodule

📊 仿真验证结果

=== 浮点比较器测试 ===
PASS 1==1
PASS +0==-0
PASS 1<2
PASS -1<1
PASS 2>1
PASS NaN==NaN (False)
PASS NaN!=NaN (True)
PASS NaN<1 (False)
PASS uno(NaN,1)
PASS -0<=+0

=== 浮点比较器测试完成,错误: 0 ===

✅Verilator验证通过

📖 比较器在不同ISA中的实现

不同架构对浮点比较的支持差异:

ISA比较指令条件码特殊处理
x86UCOMISS/COMISSEFLAGS: ZF/PF/CFCOMISS产生#IA异常
ARMFCMP/FCMPNZCV标志支持signaling比较
RISC-Vflt/fle/feq直接输出布尔feq不产生无效异常
MIPSc.lt.s/c.eq.sFCCR条件码signaling谓词
💡 条件码 vs 布尔输出:x86/ARM使用条件码(设置标志位,后续指令读取),而RISC-V直接输出布尔值到通用寄存器。后者更简单,也更适合乱序执行。

📖 比较器的异常处理

浮点比较可能产生Invalid Operation异常:

异常触发规则: Signaling: c.lt.s $1, $2 (遇NaN → 触发异常 + 返回False) Quiet: flt rd, rs1, rs2 (遇NaN → 不触发异常 + 返回False) 大多数高级语言使用Quiet版本: if (a < b) ... → 不应因NaN而崩溃

📝 练习

练习1:添加Invalid Operation异常信号输出,在signaling比较遇到NaN时置位。

练习2:实现双精度浮点比较器,注意64位的符号翻转策略。

练习3:实现浮点分类指令(fxclass),输出操作数属于哪一类(NaN/∞/normal/denorm/zero)。

练习4:构造+0和-0的完整测试集,验证所有比较谓词对它们的处理。

🏆 成就解锁

🏅 浮点比较专家

✅ 理解浮点比较与整数比较的区别

✅ 掌握符号翻转法无符号比较

✅ 实现所有7种IEEE 754比较谓词

✅ 正确处理NaN和±0的比较

✅ 实现浮点min/max选择器

✅ 理解signaling vs quiet比较