浮点运算
浮点运算
复习定位
浮点数加减法不能像整数那样直接进行——因为两个数的指数可能不同——需要先对阶(将指数对齐)——然后对尾数进行加减——再规格化结果——最后舍入。每一步都引入了误差——尤其是对阶时低位移出导致精度损失和舍入。理解浮点运算过程——才能理解为什么浮点数运算结果常常有微小的"不该出现"的误差。
浮点数加减法的五个步骤
以(1.5)₁₀+(1.25)₁₀= (2.75)₁₀为例:
1. 对阶(Align Exponent)——将两个数的指数对齐到较大的那个指数。指数较小的数的尾数右移——位数差值。
(1.5)₁₀ = 0_01111111_10000000000000000000000 (单精度 — E=127+0,R=1.1₂)
(1.25)₁₀ = 0_01111111_01000000000000000000000 (E=127+0,R=1.01₂)
两者的指数相等——无需对阶。如果指数差1——右移1位——移出的位可能丢失——引入误差。
2. 尾数相加减——将两个数对阶后的尾数(包括隐含的1)相加或相减。保持隐含位1参与计算。
3. 规格化——将结果尾数规格化为1.M的形式。如果尾数向右进位产生新的整数位(如1.1+1.0=10.1)——右移一位——指数+1。如果尾数高位为0——左移直到最高位为1——指数相应减1。
4. 舍入(Rounding)——尾数在移位或规格化过程中可能丢失了多余的位——需要根据这些位进行舍入——减少误差。
5. 判断溢出——指数是否超出可表示的最大/最小值——如果超过则输出+∞或-∞——如果低于最小则输出非规格化数或0。
对阶的误差来源
对阶是被动环节——当两个数的指数不同时——较小的指数向较大的指数看齐——尾数右移——被移出的低位丢失。
例: 1.0 + 2^(-24) 在单精度下——需要将2(-24)的尾数右移24位——全部移出有效位——结果等于1.0——而2(-24)完全被丢弃。这就是"大数+小数=大数"的浮点误差的底层原因。
三种重要舍入模式
| 模式 | 说明 | 趋向 | 适用场景 |
|---|---|---|---|
| 向最近偶数舍入 | 取最靠近的可表示值;恰好在中间时选尾数末位为偶数的 | 最接近 | IEEE 754默认 |
| 向零舍入 | 直接截断多余位 | 绝对值减小 | C语言float→int的转换 |
| 向+∞舍入 | 结果偏大 | 正方向 | 区间运算的上界 |
| 向-∞舍入 | 结果偏小 | 负方向 | 区间运算的下界 |
向最近偶数舍入的"偶数"是指尾数的最低有效位被舍入为0——而非数值为偶数。例如二进制1.00101舍入到1.001(因为1.0010更近)/舍入到1.010(因为1.0100更近)/中间值1.00110→1.010(因为最低位0变成0——偶数的判定)?
浮点运算的累积误差
多次浮点运算后——小误差可能累积成大误差。条件判断if sum == 1.0在大量浮点加法的累积后可能永远为假——即使理论上应该等于1.0。因此浮点数比较不用==——而用fabs(a - b) < epsilon。
复习检查
浮点数加法对阶——如果指数差24(即一个数比另一个数小2^24倍)——对阶后较小数的尾数完全移出有效位——结果等于较大的那个——这解释了为什么浮点数不能精确处理极大数与极小数相加。
规格化的过程中——尾数加法可能导致进位到整数位——这时需要右移一位指数加1——这种情况发生在
(1.111...1) + (0.000...1) = 10.000...0时。IEEE754四种舍入模式中——向最近偶数舍入为什么是默认——它的统计偏差最小——等距时选取偶数——长期累积的舍入误差趋向于0——而不是单向增大(如向零舍入会导致负数方向偏小)。
为什么
0.1 + 0.2 != 0.3——因为0.1和0.2的二进制是无限循环、表示时被截断为23位尾数——加法过程中又经历对阶、舍入——两次截断/舍入后的近似值——加起来不可能等于0.3的精确十进制──实际上是0.30000000000000004。浮点数的"精度"在不同区间的变化——浮点数在0附近非常密集(因为非规格化数)——在大数附近非常稀疏(指数大时尾数的步长增大)——一个在1e38附近的浮点数和下一个可表示的浮点数间隔约2e31之大。