浮点运算的精确结果通常需要比目标格式更多的位数来表示。例如,两个24位尾数相乘产生48位结果,但单精度只存储24位(含隐含1)。因此必须舍入到目标精度。
IEEE 754的核心设计原则:舍入后的结果应当等于无限精度结果舍入后的值,即"如同无限精度计算后再舍入"(as-if infinitely precise)。这意味着我们需要知道所有被移出位的信息,而不仅仅是保留部分。
IEEE 754的默认舍入模式,统计上最无偏:
总是截断,向零方向舍入:1.9→1, -1.9→-1。也叫"截断模式"(Truncation)。这是C/C++中整数转换的默认行为,也常用于除法器的中间步骤。
总是向上取整:1.1→2, -1.9→-1。也叫"天花板函数"(Ceiling)。用于区间运算的上界计算——当我们需要保证结果不会小于真值时使用。
总是向下取整:1.9→1, -1.1→-2。也叫"地板函数"(Floor)。用于区间运算的下界计算——当我们需要保证结果不会大于真值时使用。
实现舍入需要跟踪三个位,它们编码了"被移出位"的全部信息:
这三个位足以做出正确的舍入决策。关键洞察:只要知道"有没有位被移出"(Sticky),而不需要知道具体移出了什么。Sticky bit使得我们可以做到"如同无限精度计算后舍入"。
单精度(p=24):最大相对误差 ≈ 2^(-24) ≈ 5.96×10⁻⁸
双精度(p=53):最大相对误差 ≈ 2^(-53) ≈ 1.11×10⁻¹⁶
机器epsilon是浮点运算中"最小可感知差异"的度量。任何运算a OP b,如果|b| < ε|a|,则a+b在数值上可能等于a。
大数的绝对误差大,小数的绝对误差小——但相对误差恒定。这就是浮点数"等相对精度"的本质。
| 模式 | 编码 | 进位条件 | 特点 |
|---|---|---|---|
| RNE | 2'b00 | G & (R | S | LSB) | 默认模式,统计无偏 |
| RZ | 2'b01 | 永不进位 | 截断,绝对值最小 |
| RP | 2'b10 | ~sign & (G | R | S) | 正数进位,负数截断 |
| RN | 2'b11 | sign & (G | R | S) | 负数进位,正数截断 |
舍入操作在FPU流水线中处于关键位置——它必须等待G/R/S位全部计算完成后才能执行。在加法器中,G/R/S的最终值可能在规格化移位后才确定,这使得舍入成为延迟的重要贡献者。
舍入操作在FPU流水线中处于关键位置——它必须等待G/R/S位全部计算完成后才能执行。在加法器中,G/R/S的最终值可能在规格化移位后才确定,这使得舍入成为延迟的重要贡献者。
双路计算(Dual-Path)是最常用的优化:同时计算round_up=0和round_up=1两个结果,然后用一个MUX选择。虽然面积增加约30%,但延迟减少一个周期。在现代高性能FPU中几乎标配。
舍入不仅仅是精度问题,它直接影响数值算法的稳定性:
舍入中的"加1"操作可能触发进位传播(carry ripple),导致尾数位宽增加1位:
这就是"舍入后二次规格化"的来源。在硬件中,必须处理以下连锁反应:舍入进位 → 尾数溢出 → 指数+1 → 指数溢出。这种级联是FPU设计中最微妙的边界情况之一。
| 运算 | 精确结果位宽 | 需要舍入? | G/R/S来源 |
|---|---|---|---|
| 加法(同号) | 25位 | 可能 | 对齐移出位 |
| 减法(异号) | 24位 | 可能 | 规格化左移后 |
| 乘法 | 48位 | 总是 | 乘积低24位 |
| 除法 | ∞位 | 总是 | 商的余数 |
| 平方根 | ∞位 | 总是 | 余数 |
| FMA | ∞位 | 总是 | 乘积累加后的低段 |
乘法和除法总是需要舍入(因为精确结果通常无法用有限位表示),而加减法只在特定情况下需要。
// float_round.sv - IEEE 754 通用舍入单元
// 支持4种舍入模式: RNE/RZ/RP/RN
module float_round #(parameter MAN_WIDTH=24)(
input wire [MAN_WIDTH:0] mant_in,
input wire guard, round_bit, sticky, sign,
input wire [1:0] round_mode,
output wire [MAN_WIDTH-1:0] mant_out,
output wire overflow
);
wire lsb = mant_in[0];
wire round_up_rne = guard & (round_bit | sticky | lsb);
wire round_up_rz = 1'b0;
wire round_up_rp = ~sign & (guard | round_bit | sticky);
wire round_up_rn = sign & (guard | round_bit | sticky);
wire do_round_up = (round_mode==2'b00)?round_up_rne:(round_mode==2'b01)?round_up_rz:(round_mode==2'b10)?round_up_rp:round_up_rn;
wire [MAN_WIDTH:0] mant_inc = mant_in + {{(MAN_WIDTH){1'b0}},1'b1};
wire [MAN_WIDTH:0] mant_rounded = do_round_up ? mant_inc : {1'b0,mant_in[MAN_WIDTH-1:0]};
assign mant_out = mant_rounded[MAN_WIDTH-1:0];
assign overflow = mant_rounded[MAN_WIDTH];
endmodule=== 舍入测试 ===
1.101_10 RNE→1.110 ✓
1.100_10 RNE→1.100 ✓
1.011_01 RNE→1.011 ✓
RZ截断 ✓ RP/RN方向 ✓✅Verilator验证通过
练习1:用二进制说明RNE向偶数决策
练习2:验证4种舍入模式
练习3:RZ系统性偏差分析
练习4:双精度舍入单元(MAN_WIDTH=53)
✅ 4种模式数学定义
✅ G/R/S计算方法
✅ 通用舍入单元
✅ 舍入误差统计特性