想象你是一家跨国物流公司的行李打包员。你的任务是把各种各样的数字装进一只只有 32 个货格的行李箱:有账本上精确到分的 42.5,有银行大额存单上的 300000,有科学家口中 1e30 的天文数字,还有 0.1 元这种看似普通的小数目。怎么才能让所有数字都塞进这 32 格? 第一种做法叫“定点”:把箱子里的隔板焊死,前 16 格放整数部分,后 16 格放小数部分,小数点位置永远固定。好处是整齐、装得快、打开就能直接算;坏处是 300000 这种大数字根本塞不进去,只能“爆箱”溢出。 第二种做法叫“浮点”:仿照科学计数法,把数字拆成“符号 + 指数 + 尾数”三张标签。指数标签记录小数点该往哪儿挪,于是天文数字和显微数字都能装下。但尾数只有 23 格,像 0.1 这种二进制里永远写不完的小数,只能被“压扁”成近似值——这就是浮点数一切“精度陷阱”的根源。 这篇文章带你拆开这两只行李箱,看懂 IEEE 754 标准如何设计、为什么 0.1 + 0.2 ≠ 0.3,以及工程上如何避坑。

岗位关联

理解定点与浮点,是 AI 工程师调试模型精度、定位 NaN 异常、优化混合精度训练与推理性能的必修课——从 softmax 的数值稳定性到梯度累积,处处都是它的影子。

目录
  1. 可视化演示
  2. 定点数基础——焊死的隔层行李箱
  3. 浮点数格式——科学计数法标签
  4. IEEE 754 规范——偏置与特殊标签
  5. 舍入与精度——装不下的布料
  6. 浮点运算规则——对阶与尾数计算
  7. 应用与陷阱——什么行李用什么箱子
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

定点数基础——焊死的隔层行李箱

第一只行李箱出厂时就把隔板焊死了:前 16 格统一放整数,后 16 格统一放小数,小数点的位置雷打不动。这种“小数点位置固定”的编码就是定点数。常见形态有 Q15(1 位符号 + 15 位小数)和 16.16(16 位整数 + 16 位小数)。为了让负数也能装进格子,硬件并不专门用一格写正负号,而是采用补码:最高位拥有“负的权重”,这一位是 1 时直接贡献一个负的大数。

n 位补码数的取值可以统一写成:最高位按负权重、其余位按正权重相加。

所以定点行李箱的“容量”在出厂时就焊死了。16.16 无符号格式最多装 65535.9999…,300000 一塞就爆箱,这正是上方动画里红色“溢出”的含义。定点数硬件极简、速度飞快,但范围弹性为零——这是它最大的脾气。

V=−an−1×2n−1+∑i=0n−2ai×2i\displaystyle V = -a_{n-1}\times 2^{n-1} + \sum_{i=0}^{n-2} a_i \times 2^{i}
  1. 符号位取负权重最高位 \(a_{n-1}\) 的权重是负的 \(2^{n-1}\),所以它为 1 时贡献一个很大的负数,这正是补码负数比正数多一个的原因
    −an−1×2n−1\displaystyle -a_{n-1}\times 2^{n-1}
  2. 其余位正常加权低 n-1 位每一位仍按普通二进制记数,0 或 1 乘以对应的 2 的幂
    ∑i=0n−2ai×2i\displaystyle \sum_{i=0}^{n-2} a_i \times 2^{i}
  3. 范围由位数决定32 位补码的最小值是符号位为 1 其余全为 0,最大值是符号位为 0 其余全为 1
    −231≤V≤231−1\displaystyle -2^{31} \le V \le 2^{31}-1

浮点数格式——科学计数法标签

聪明的打包员从车站电子屏得到启发:“光速是 3.0×10⁸ m/s”绝不会写成 300000000。他们把小数点挪到第一个有效数字后面,再用指数记录挪了几格。行李箱也改成三张标签:1 格写正负号(S),8 格写指数(E),23 格写尾数(M)。这就是 IEEE 754 单精度(float32);双精度(float64)则是 1 位符号 + 11 位指数 + 52 位尾数。

所谓“规格化”,是让所有数字都写成 \(1.xxx\times 2^{y}\) 的形式。小数点前面永远只有一位 1,于是这个 1 可以偷偷省掉不存,白赚一格精度,这就是“隐式前导位”。

单精度最大约 \(3.4\times 10^{38}\),最小规格化数约 \(1.2\times 10^{-38}\),比定点的 ±65535 大了不知多少量级。隔板虽然能滑动,但尾数格永远只有 23 格——行李的“有效数字”位数没变,只是能搬动小数点的位置而已。

V=(−1)s×(1.M)2×2 E−127\displaystyle V = (-1)^{s} \times (1.M)_2 \times 2^{\,E-127}
  1. 符号位决定正负一格标签,\(s=1\) 表示负数
    (−1)s\displaystyle (-1)^{s}
  2. 尾数加隐含的 123 格尾数拼在"1."后面,形成一个 24 位有效数字
    (1.M)2=1+∑i=123bi×2−i\displaystyle (1.M)_2 = 1 + \sum_{i=1}^{23} b_i \times 2^{-i}
  3. 指数减去偏置 127存进去的 E 是无符号整数,真实指数是 E-127,这样指数可正可负
    2 E−127\displaystyle 2^{\,E-127}

IEEE 754 规范——偏置与特殊标签

指数标签的设计很巧妙:它不存真实指数,而是存“真实指数 + 127”这个偏置值。这就像旅馆把房间号统一从 101 编起,让“负数楼层”也能用正数房卡开门。此外,协议还预留了几张特殊标签:全 0 指向“地下室特小件”(非规格化数),全 1 指向“无限远行李”(Inf)和“事故行李”(NaN)。

具体来说,单精度指数 E 分四种情况:E=255 且尾数为 0 表示 ±∞(溢出);E=255 且尾数非 0 表示 NaN(非法运算,如 0/0);E 在 1~254 之间是规格化数;E=0 且尾数是 0 表示 ±0,E=0 且尾数非 0 则是非规格化数。

非规格化数实现了“渐进式下溢”:数字不会突然从最小规格化数跳到 0,避免了除零灾难。而 NaN 有个著名的反直觉特性——NaN ≠ NaN,因为“事故行李”之间没法比大小。有了偏置和特殊值,这套行李箱协议才算完整:普通行李、地下室特小件、无限远票据、事故凭证,各就各位。

V=(−1)s×2 1−127×(0.M)2=(−1)s×2−126×(0.M)2\displaystyle V = (-1)^{s} \times 2^{\,1-127} \times (0.M)_2 = (-1)^{s} \times 2^{-126} \times (0.M)_2
  1. 指数固定为 -126非规格化数的指数统一按 \(1-127\) 计算,而不是 \(0-127\),这是为了和最小规格化数平滑衔接
    2 1−127=2−126\displaystyle 2^{\,1-127} = 2^{-126}
  2. 前导位降为 0尾数不再隐含 1,直接用 0.M 表示,一步步填满 0 附近的空隙
    (0.M)2=∑i=123bi×2−i\displaystyle (0.M)_2 = \sum_{i=1}^{23} b_i \times 2^{-i}

舍入与精度——装不下的布料

尾数只有 23 格,可有些数字的二进制展开写不完,就像一块布比行李箱长出一截,必须剪掉。怎么剪?IEEE 754 默认采用“舍入到最近偶数”:多出的长度不到半格就剪,超过半格就进位,恰好卡在中间则剪向末位为 0 的那个格子——这样统计上偏差最小。另外还有向 +∞、向 -∞、向 0 三种舍入模式。

因为浮点数是离散分布的,相邻两个可表示数的间距叫 ULP(unit in the last place,末位单位)。它衡量了“剪布”的最小刻度。

剪布带来的两个著名后果:一是 0.1 在 float32 里实际是 0.10000000149…;二是灾难性抵消——两个相近的大数相减,如 1.0000001 - 1.0,有效位几乎全丢,好比两座大山相减只剩一粒沙,而这粒沙自身的误差可能比它还大。剪布不可怕,可怕的是以为剪完还是原尺寸。

ulp=2 e−23\displaystyle \text{ulp} = 2^{\,e-23}
  1. 尾数最低位权重真实指数为 e 时,尾数第 23 位(最低位)代表的数值就是 \(2^{e-23}\)
    2 e−23\displaystyle 2^{\,e-23}
  2. 舍入误差上界默认舍入模式的误差不超过半格
    ∣x−x~∣≤12 ulp=2 e−24\displaystyle |x - \tilde{x}| \le \tfrac{1}{2}\,\text{ulp} = 2^{\,e-24}

浮点运算规则——对阶与尾数计算

两只行李箱的货物要相加,前提是小数点对齐。假如 \(1.0\times 2^{3}\) 和 \(1.1\times 2^{0}\) 相加,必须先把后者“齐步走”右移 3 格变成 \(0.0011\times 2^{3}\),再逐格加尾数。右移时多出去的低位就被剪掉了——这就是“大数吃小数”的由来。

浮点加减法共四步:①对阶(小阶向大阶看齐);②尾数相加或相减;③规格化(进位溢出就右移,前导位为 0 就左移);④舍入。乘除法更简单:指数相加(或相减),尾数相乘(或相除),再做一次规格化。

运算过程中还有异常处理:指数超过 254 就变成 ±Inf,0/0 或 Inf−Inf 变成 NaN,下溢先退化成非规格化数、再小就变成 0。Python 和 JavaScript 默认静默处理这些异常,所以经常“眼前无事,数值已脏”——每一次对阶都是一次剪布,运算次数越多,误差越积越厚。

Δe=e1−e2,M=M1±M2\displaystyle \Delta e = e_1 - e_2, \qquad M = M_1 \pm M_2
  1. 对阶差值两个数的指数差决定较小的尾数要右移多少位
    Δe=e1−e2\displaystyle \Delta e = e_1 - e_2
  2. 尾数对位相加统一阶码后,23 位尾数直接做加减
    M=M1±M2\displaystyle M = M_1 \pm M_2
  3. 结果再规格化若 \(M \ge 2\) 则整体右移一位、指数加一;若 \(M < 1\) 则左移、指数减一
    M=1.f×2 e′\displaystyle M = 1.f \times 2^{\,e'}

应用与陷阱——什么行李用什么箱子

老打包员总结的规矩是:行李箱不是越高级越好。定点箱便宜、结实、速度极快,适合规格统一的流水线;浮点箱灵活、范围巨大,适合来者不拒的通用柜台。工程上,音频编解码、通信基带滤波器、电机控制等嵌入式场景几乎全用定点数,因为省掉浮点单元(FPU)能大幅降低功耗和芯片面积;而科学仿真、AI 训练、数据库等场景则必须用浮点,double 能提供 15~17 位十进制有效数字。

浮点数最经典的陷阱是判等:0.1 + 0.2 在 IEEE 754 下并不等于 0.3。正确姿势是带容差比较:

针对累加误差,还有一招 Kahan 补偿求和:把每次相加丢掉的误差悄悄记在小账本上,下一轮补回来,能显著缓解大数吃小数。理解了两只行李箱的脾气,你就能在 AI 训练中一眼看出“loss 变成 NaN”是 Inf/Inf 事故,“梯度不更新”可能是大数吃小数的精度陷阱——对症下药,而不是怀疑人生。

∣a−b∣<ε⟹a≈b\displaystyle |a - b| < \varepsilon \quad \Longrightarrow \quad a \approx b
  1. 绝对误差阈值当两数差距小于某个微小量 ε 时,认为它们近似相等
    ∣a−b∣<ε\displaystyle |a - b| < \varepsilon
  2. ε 按精度选取float32 约有 7 位有效数字,取 \(10^{-7}\);float64 取 \(10^{-15}\) 量级
    ε=10−7 或 10−15\displaystyle \varepsilon = 10^{-7} \text{ 或 } 10^{-15}

AI 应用场景

嵌入式音频 DSP 的定点实现

阶段:模型部署 / 推理优化

解决的问题:助听器、蓝牙耳机等端侧设备的 MCU 没有浮点单元,浮点计算又慢又费电。算法工程师把神经网络权重和音频信号全部定点化(Q15 格式),用整数乘加完成 FIR 滤波和自适应降噪,功耗能降一个数量级。

具体案例:某助听器芯片用 16 位定点实现深度学习降噪,采样率 16 kHz,每个样本约 200 次整数乘加,整机电流从 2 mA 降到 0.3 mA,电池续航翻倍。

科学计算中的数值稳定性

阶段:数值算法实现

解决的问题:地震波模拟、气象预报等场景要对海量格点连续累加,直接求和会让误差像滚雪球一样漂移。工程师用双精度加 Kahan 补偿求和,用“小账本”把每次丢弃的误差补回来。

具体案例:某大气模型对 10⁷ 个格点的辐射通量求和,朴素累加误差达 1e-5,改用 Kahan 补偿后误差降到 1e-12,模拟结果从“定性正确”升级为“定量可信”。

大模型混合精度训练

阶段:训练

解决的问题:训练大语言模型时用 float16 能省一半显存和带宽,但梯度太小会直接下溢成 0,或偶尔冒出 NaN 导致训练崩溃。工程师用 bf16 前向 + float32 主权重 + 梯度累积来解决。

具体案例:某 LLM 训练做学习率扫描时发现,小学习率下梯度被“大数吃小数”吞掉,参数长时间不更新;改用梯度累积和 loss scaling 后,loss 曲线恢复正常下降。

Python 演示

python33 行
import struct def f32(x):    # 把 Python float 按 float32 重新打包,再拆成 32 位整数    bits = struct.unpack('>I', struct.pack('>f', x))[0]    return (bits >> 31) & 1, (bits >> 23) & 0xFF, bits & 0x7FFFFF def decode(s, e, m):    # 按 IEEE 754 规则把位模式还原成数值    if e == 255:        return float('inf') if m == 0 else float('nan')    if e == 0:        return (-1) ** s * (m / 2 ** 23) * 2 ** -126    return (-1) ** s * (1 + m / 2 ** 23) * 2 ** (e - 127) def fixed16_16(x):    # 16.16 定点编码:把小数放大 2^16 后取整    if not (0 <= x <= 65535 + 65535 / 65536):        return None    return (int(round(x * 65536)) & 0xFFFFFFFF) def main():    print('0.1 + 0.2 == 0.3:', 0.1 + 0.2 == 0.3)    print('0.1 + 0.2 实际值:', repr(0.1 + 0.2))    for v in [0.1, 1.0, 42.5, 65535.0, 300000.0, 1e30]:        s, e, m = f32(v)        print('%-10g -> s=%d e=%3d m=0x%06X 还原=%.8g' % (v, s, e, m, decode(s, e, m)))    for v in [42.5, 300000.0]:        enc = fixed16_16(v)        print('定点16.16 编码 %-8g ->' % v, hex(enc) if enc is not None else 'overflow') if __name__ == '__main__':    main()
目的:用 Python 亲手验证 IEEE 754 的位拆解与定点溢出,直观看到 0.1+0.2≠0.3 的原因。
  1. bits = struct.unpack('>I', struct.pack('>f', x))[0]把 float32 的底层 32 位原样读成整数,相当于“翻开行李箱看内部格子”。
  2. (bits >> 31) & 1, (bits >> 23) & 0xFF, bits & 0x7FFFFF依次取出最顶 1 格(符号)、中间 8 格(指数)、低 23 格(尾数)。
  3. if e == 0:非规格化数分支,隐式前导位是 0,真实指数固定 -126。
  4. return (-1) ** s * (1 + m / 2 ** 23) * 2 ** (e - 127)规格化数把隐藏的"1."补回来,再按公式还原。
  5. if not (0 <= x <= 65535 + 65535 / 65536):定点范围检测,超出 16.16 上限就报告溢出。
  6. return (int(round(x * 65536)) & 0xFFFFFFFF)定点编码的本质:把小数部分放大 2¹⁶,四舍五入后塞进 32 位整数。
  7. print('0.1 + 0.2 == 0.3:', 0.1 + 0.2 == 0.3)一眼看穿浮点判等陷阱。
  8. s, e, m = f32(v)对每个测试数做位拆解,观察范围与精度的变化。

常见误区

误区float 有 32 位甚至 64 位,位数那么多,肯定比定点数存得准。

正解浮点只是把位数分给了指数,换来了巨大范围;尾数只有 23 位,相对精度约 7 位十进制有效数字。在 ±65535 内,16.16 定点的绝对误差约 1.5e-5,而 float32 在 65535 附近的误差约 0.004,定点反而更稳。就像浮点行李箱把更多格子让给了“楼层号”,存放细软的格子反而更少。

为何常见:把“可表示的数更多”等同于“更精确”,忽略了范围与精度是两回事。

误区一定是我代码写错了,或者 Python/JS 的 bug,换个语言就好了。

正解这是 IEEE 754 的固有属性。0.1 和 0.2 的二进制展开是无限循环小数,任何二进制浮点(任何语言)存 0.1 都有误差。误差不是 bug,而是“剪布”的必然结果。可以用 float64 或 Decimal 减小误差,但二进制浮点无法根除。

为何常见:十进制习惯根深蒂固,没意识到计算机底层是二进制小数。

误区范围这么大,0 到 3.4e38 之间的数肯定都能精确存下来。

正解浮点数是“对数分布”的——离 0 越远越稀疏。float32 在 1e30 附近的 ULP 约 1.4e23,意味着 10³⁰ 和 10³⁰+1 存成同一个数。范围是一回事,精度是另一回事。

为何常见:把整数的“连续覆盖”直觉套到浮点上,没意识到尾数位数有限。

误区两个计算出来的浮点数,相等就是相等,直接 == 判断最可靠。

正解每次舍入都引入误差,两条殊途同归的计算路径可能结果差最后一位。正确姿势是容差比较 \(|a-b| < \varepsilon\)。特殊值例外:NaN 不等于任何数包括自己,判断必须用 math.isnan。

为何常见:教学从整数开始,"==" 用得顺手,忽略了表示误差的累积。

面试题

Q1. 在 IEEE 754 单精度浮点数中,32 位是如何分配的?

单选题 · easy

  • A.1位符号+8位指数+23位尾数← 正确答案
  • B.1位符号+11位指数+52位尾数
  • C.1位符号+8位指数+24位尾数
  • D.1位符号+7位指数+24位尾数

答案:A。单精度结构是 1+8+23,双精度才是 1+11+52。C 错在尾数存储只有 23 位(另 1 位是隐含的 1),D 错在指数位不是 7。

Q2. 单精度 float32 的指数偏置(bias)值是多少?

单选题 · easy

  • A.1023
  • B.127← 正确答案
  • C.255
  • D.256

答案:B。单精度 bias=127,双精度 bias=1023。偏置的作用是把可能为负的真实指数变成非负存储,方便硬件按字典序比较大小,就像给房间号统一加 100 再编号。

Q3. 十进制数 0.1 存入 float32 后会发生什么?

单选题 · mid

  • A.二进制完全可以精确存储
  • B.只能舍入成近似值存储← 正确答案
  • C.因为太小而下溢为零
  • D.超出尾数范围而溢出

答案:B。0.1 的二进制展开是无限循环小数 0.0001100110011…,23 位尾数装不下,按默认舍入模式截断,实际存为 0.10000000149。下溢只发生在指数小于 -126 的数上,0.1 远未下溢。

Q4. 规格化浮点数中,尾数隐含的前导位是什么?

单选题 · mid

  • A.始终是 0
  • B.始终是 1← 正确答案
  • C.由符号位决定
  • D.由指数决定

答案:B。规格化数写作 1.xxx × 2^E,前导的 1 被隐含省略以节省一格尾数;只有非规格化数(E=0)的前导位才是 0。

Q5. IEEE 754 中指数位全为 1 且尾数为 0 表示什么?

单选题 · mid

  • A.表示数字零值
  • B.表示无穷大数← 正确答案
  • C.表示非法数值
  • D.表示最小规格化数

答案:B。E=255 且 M=0 表示 ±∞(溢出结果);E=255 且 M≠0 才是 NaN;E=0 且 M=0 才是零;最小规格化数对应 E=1。

Q6. 采用 1 位符号 + 15 位整数 + 16 位小数的定点格式,能表示的最大正数是?

单选题 · mid

  • A.65535.9999
  • B.32767.9999← 正确答案
  • C.32768.0000
  • D.65536.0000

答案:B。15 位整数部分最大是 2¹⁵-1=32767,再加 16 位小数的最大近似 1,因此是 32767.9999…。注意最高位是符号位,不参与数值大小。

Q7. 下面关于 0.1+0.2 的说法,正确的是?

单选题 · mid

  • A.0.1+0.2 等于 0.3
  • B.0.1+0.2 约等于 0.3← 正确答案
  • C.0.1+0.2 大于 0.3
  • D.0.1+0.2 小于 0.3

答案:B。0.1 和 0.2 的二进制近似值相加后,按舍入规则得到 0.30000000000000004,略大于数学上的 0.3,所以“约等于”最准确。这是浮点误差的经典考题。

Q8. 下列哪些情况会造成明显的浮点精度损失?

多选题 · mid

  • A.大数与小数直接相加← 正确答案
  • B.相近的大数互相相减← 正确答案
  • C.乘以 2 的整数次幂
  • D.使用 Kahan 补偿求和

答案:AB。大数加小数时,对阶右移会把小数尾数挤出有效位;相近大数相减会触发灾难性抵消。乘以 2 的幂只是指数平移,完全无损;Kahan 求和是减少误差的手段而非来源。

Q9. 关于浮点数比较,下列说法正确的是?

多选题 · mid

  • A.判等用 == 最准确
  • B.比较时应使用容差← 正确答案
  • C.NaN 不等于包括自己← 正确答案
  • D.0.1 在 float32 中精确

答案:BC。浮点数有表示误差,== 只在两条计算路径完全一致时成立,正确做法是 \(|a-b|<\varepsilon\)。NaN 的“自己不等于自己”是 IEEE 754 的明确规定,用于表示未定义结果。0.1 永远无法精确存储。

Q10. 关于 IEEE 754 的规格化与非规格化数,下列说法正确的是?

多选题 · hard

  • A.非规格化数填补了 0 附近的空隙← 正确答案
  • B.非规格化数的隐式前导位是 1
  • C.规格化数的最小指数是 -126← 正确答案
  • D.指数偏置使得指数只能为正

答案:AC。非规格化数(E=0,M≠0)用 0.M 表示,让 0 到最小规格化数之间平滑过渡,所以前导位是 0 而非 1。规格化数 E 最小为 1,真实指数 = 1-127 = -126。偏置的目的恰恰是让指数可正可负,D 说反了。