📘 第05课:舍入模式

📖 为什么需要舍入?

浮点运算的精确结果通常需要比目标格式更多的位数来表示。例如,两个24位尾数相乘产生48位结果,但单精度只存储24位(含隐含1)。因此必须舍入到目标精度。

精确结果: 1.1011001100110011001100110011... (无限精度) ↑ ↑ 保留位(24位) 舍入位 目标: 在保留位与舍入位的边界处进行舍入

IEEE 754的核心设计原则:舍入后的结果应当等于无限精度结果舍入后的值,即"如同无限精度计算后再舍入"(as-if infinitely precise)。这意味着我们需要知道所有被移出位的信息,而不仅仅是保留部分。

📖 四种舍入模式详解

1. 向最近偶数舍入(Round to Nearest Even, RNE)

IEEE 754的默认舍入模式,统计上最无偏:

示例(保留到整数): 1.4 → 1 (0.4 < 0.5, 向下) 1.6 → 2 (0.6 > 0.5, 向上) 1.5 → 2 (= 0.5, 向偶数: 2是偶数) 2.5 → 2 (= 0.5, 向偶数: 2是偶数, 不是3!) 3.5 → 4 (= 0.5, 向偶数: 4是偶数)
💡 "向偶数"的意义:在大量运算的统计意义上,向上和向下舍入的次数大约相等,避免了系统性偏差。这是IEEE 754选择此模式作为默认的原因。统计研究表明,RNE在大多数应用中产生的累积误差最小。

2. 向零舍入(Round toward Zero, RZ)

总是截断,向零方向舍入:1.9→1, -1.9→-1。也叫"截断模式"(Truncation)。这是C/C++中整数转换的默认行为,也常用于除法器的中间步骤。

3. 向正无穷舍入(Round toward +∞, RP)

总是向上取整:1.1→2, -1.9→-1。也叫"天花板函数"(Ceiling)。用于区间运算的上界计算——当我们需要保证结果不会小于真值时使用。

4. 向负无穷舍入(Round toward -∞, RN)

总是向下取整:1.9→1, -1.1→-2。也叫"地板函数"(Floor)。用于区间运算的下界计算——当我们需要保证结果不会大于真值时使用。

📖 舍入的三个关键位:G/R/S

实现舍入需要跟踪三个位,它们编码了"被移出位"的全部信息:

Guard bit (G) - 保留位后的第1位 Round bit (R) - 保留位后的第2位 Sticky bit (S) - 保留位后第3位及以后的OR结果 示例: 1.101_1011... 需要舍入到4位 保留: 1.101 G=1, R=0, S=11...→1 (sticky是OR) RNE判断逻辑: G=0 → 截断 (0.5 ULP以下) G=1, (R|S)=1 → 进位 (0.5 ULP以上) G=1, (R|S)=0 → 向偶数 (恰好在中间) 保留部分最低位(LSB)=1 → 进位 (使变偶) 保留部分最低位(LSB)=0 → 截断 (已是偶)

这三个位足以做出正确的舍入决策。关键洞察:只要知道"有没有位被移出"(Sticky),而不需要知道具体移出了什么。Sticky bit使得我们可以做到"如同无限精度计算后舍入"。

📖 舍入误差分析

相对舍入误差上界

|相对误差| ≤ 2^(-p) (RNE模式),其中p为有效位数

单精度(p=24):最大相对误差 ≈ 2^(-24) ≈ 5.96×10⁻⁸

双精度(p=53):最大相对误差 ≈ 2^(-53) ≈ 1.11×10⁻¹⁶

机器epsilon

ε = 2^(-(p-1)) → 单精度: 2^(-23) ≈ 1.19×10⁻⁷, 双精度: 2^(-52) ≈ 2.22×10⁻¹⁶

机器epsilon是浮点运算中"最小可感知差异"的度量。任何运算a OP b,如果|b| < ε|a|,则a+b在数值上可能等于a。

绝对误差与ULP

绝对误差 ≤ 0.5 × ULP = 0.5 × 2^(e-p+1)

大数的绝对误差大,小数的绝对误差小——但相对误差恒定。这就是浮点数"等相对精度"的本质。

📖 各舍入模式的Verilog判断逻辑汇总

模式编码进位条件特点
RNE2'b00G & (R | S | LSB)默认模式,统计无偏
RZ2'b01永不进位截断,绝对值最小
RP2'b10~sign & (G | R | S)正数进位,负数截断
RN2'b11sign & (G | R | S)负数进位,正数截断
⚠️ 方向舍入的符号依赖:RP和RN模式的舍入决策依赖结果的符号。在硬件中,符号在舍入前就已知(由操作数符号和运算类型决定),所以不会造成延迟问题。但要注意:不同舍入模式下,x-x可能产生+0或-0。

📖 舍入与FPU关键路径

舍入操作在FPU流水线中处于关键位置——它必须等待G/R/S位全部计算完成后才能执行。在加法器中,G/R/S的最终值可能在规格化移位后才确定,这使得舍入成为延迟的重要贡献者。

FPU流水线中的舍入位置: 对齐 → 加减 → 规格化 → 【舍入】 → 结果组装 ↑ 关键路径点 优化策略: 1. 预测舍入:在规格化前预测G/R/S,并行执行 2. 双路计算:同时计算"截断"和"进位"两个结果,然后选择 3. 舍入融合:将舍入逻辑与规格化合并

📖 舍入与FPU关键路径

舍入操作在FPU流水线中处于关键位置——它必须等待G/R/S位全部计算完成后才能执行。在加法器中,G/R/S的最终值可能在规格化移位后才确定,这使得舍入成为延迟的重要贡献者。

FPU流水线中的舍入位置: 对齐 → 加减 → 规格化 → 【舍入】 → 结果组装 ↑ 关键路径点 优化策略: 1. 预测舍入:在规格化前预测G/R/S,并行执行 2. 双路计算:同时计算"截断"和"进位"两个结果,然后选择 3. 舍入融合:将舍入逻辑与规格化合并

双路计算(Dual-Path)是最常用的优化:同时计算round_up=0和round_up=1两个结果,然后用一个MUX选择。虽然面积增加约30%,但延迟减少一个周期。在现代高性能FPU中几乎标配。

📖 舍入对数值稳定性的影响

舍入不仅仅是精度问题,它直接影响数值算法的稳定性:

💡 数值线性代数建议:使用双精度进行矩阵运算,使用RNE模式,避免不必要的中间舍入。FMA指令(第16-19课)可以显著减少舍入次数。

📖 舍入的实现细节:进位传播

舍入中的"加1"操作可能触发进位传播(carry ripple),导致尾数位宽增加1位:

尾数: 1.11111111111111111111111 (全1) 舍入进位: + 0.00000000000000000000001 结果: 10.00000000000000000000000 (25位!) 需要右移1位: 1.00000000000000000000000 同时指数+1 如果指数已经是254 → 溢出 → 返回±∞

这就是"舍入后二次规格化"的来源。在硬件中,必须处理以下连锁反应:舍入进位 → 尾数溢出 → 指数+1 → 指数溢出。这种级联是FPU设计中最微妙的边界情况之一。

📖 不同运算的舍入需求

运算精确结果位宽需要舍入?G/R/S来源
加法(同号)25位可能对齐移出位
减法(异号)24位可能规格化左移后
乘法48位总是乘积低24位
除法∞位总是商的余数
平方根∞位总是余数
FMA∞位总是乘积累加后的低段

乘法和除法总是需要舍入(因为精确结果通常无法用有限位表示),而加减法只在特定情况下需要。

🔧 Verilog实现

// 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计算方法

✅ 通用舍入单元

✅ 舍入误差统计特性