2. 浮点数据格式:单精度与双精度的二进制表示
好,咱们正式开始讲浮点数。说实话,这是整个FPU设计里最基础、也最容易踩坑的地方。我记得刚入行那会儿,总觉得浮点数不就是小数点能移动嘛,有什么难的?结果第一次做双精度乘法的时候,被NaN传播坑得差点重做整个模块。
今天咱们就把单精度(float32)和双精度(float64)的二进制表示彻底讲透。包括规格化数、非规格化数,还有那些让人头疼的特殊值——NaN、Infinity、Zero。
2.1 浮点数的二进制骨架
IEEE 754标准规定了浮点数的三部分结构:符号位、指数位、尾数位。说白了,就是用科学计数法存二进制数。
| 格式 | 总位数 | 符号位 | 指数位 | 尾数位 | 偏置值 |
|---|---|---|---|---|---|
| 单精度(float32) | 32 | 1 | 8 | 23 | 127 |
| 双精度(float64) | 64 | 1 | 11 | 52 | 1023 |
你想想看,为什么指数要加偏置?因为指数可以是负数。用偏置值之后,实际指数 = 存储指数 - 偏置值。比如单精度里存的是130,那实际指数就是130-127=3。
核心公式:
数值 = (-1)^符号 × 2^(指数-偏置) × 1.尾数
注意那个"1."——这是规格化数的隐含前导位。
2.2 规格化数:最常见的浮点数
规格化数,说白了就是正常范围内的浮点数。它的指数位既不全0也不全1。尾数前面隐含一个"1.",所以实际精度比存储位数多1位。
举个例子,单精度下表示3.14:
3.14 = 11.00100011110101110000101...(二进制)
= 1.100100011110101110000101 × 2^1
符号位:0
指数:1 + 127 = 128 = 10000000
尾数:10010001111010111000010(截断到23位)
最终:0 10000000 10010001111010111000010
我在项目中遇到过一个问题:很多人以为浮点数能精确表示所有小数。其实不能。3.14在二进制里是无限循环的,只能截断。这就是精度损失的根源。
2.3 非规格化数:处理下溢的救星
为什么会需要非规格化数?因为规格化数的最小正数是2^(-126) ≈ 1.17×10^(-38)。比这个还小的数怎么办?直接变0?那精度损失太大了。
非规格化数的规则很简单:指数位全0,尾数前面的隐含位变成"0."而不是"1."。这样就能表示更小的数了。
我个人习惯:非规格化数在硬件里实现起来特别麻烦。很多低功耗设计会直接flush to zero,也就是把非规格化数当0处理。但如果你做科学计算,千万别这么干——精度会崩。
非规格化数的范围:从2^(-149)到(1-2^(-23))×2^(-126)。注意,非规格化数的指数固定为-126(单精度),但前导位是0,所以实际指数是-126而不是-127。
2.4 特殊值:NaN、Infinity、Zero
嗯,这里要注意。特殊值的编码规则是IEEE 754里最容易搞混的部分。我当年第一次写FPU的时候,把NaN的尾数判断条件写反了,结果仿真跑了三天才发现。
| 类型 | 指数位 | 尾数位 | 说明 |
|---|---|---|---|
| +0 | 全0 | 全0 | 符号位0 |
| -0 | 全0 | 全0 | 符号位1 |
| +∞ | 全1 | 全0 | 符号位0 |
| -∞ | 全1 | 全0 | 符号位1 |
| SNaN | 全1 | 非0且最高位为0 | 信号NaN,触发异常 |
| QNaN | 全1 | 非0且最高位为1 | 安静NaN,传递错误 |
我曾经踩过的坑:在比较NaN的时候,NaN不等于任何数,包括它自己。也就是说,NaN == NaN 的结果是false。如果你在硬件里用比较器直接判断,会出大问题。必须单独检测NaN的编码模式。
为什么要有两种NaN?SNaN用于调试,一旦运算中出现SNaN,系统会触发异常。QNaN则默默传递,比如任何运算只要有一个操作数是QNaN,结果就是QNaN。我个人建议:在正式产品里,尽量用QNaN,SNaN容易把系统搞崩。
2.5 双精度的特殊之处
双精度和单精度的编码逻辑完全一样,只是位数不同。双精度的指数位11位,偏置1023,尾数52位。范围更大,精度更高。
举个例子,双精度下表示1.0:
1.0 = 1.000... × 2^0
符号位:0
指数:0 + 1023 = 1023 = 01111111111
尾数:全0
最终:0 01111111111 0000000000000000000000000000000000000000000000000000
你想想看,双精度的非规格化数最小能到2^(-1074),大约是5×10^(-324)。这个范围对大多数应用都够了。但如果你做天文计算或者量子模拟,可能还需要扩展精度格式。
2.6 实战中的编码检查
写FPU的时候,我建议你做一个编码检查模块。输入一个32位或64位的二进制数,输出它的类型:是规格化数、非规格化数、零、无穷大还是NaN。
// 伪代码:单精度编码检查
if (exp == 0xFF) {
if (mant == 0) {
return (sign == 0) ? +INF : -INF;
} else {
return (mant[22] == 1) ? QNaN : SNaN;
}
} else if (exp == 0x00) {
if (mant == 0) {
return (sign == 0) ? +ZERO : -ZERO;
} else {
return DENORMAL; // 非规格化数
}
} else {
return NORMAL; // 规格化数
}
一个小技巧:在Verilog或SystemVerilog里,判断全0全1可以用归约操作。比如 &exp 判断是否全1,|exp 判断是否全0。这样写出来的代码又简洁又不容易出错。
好了,浮点数的二进制表示就讲到这里。下一章咱们开始讲浮点加法器的设计——那才是真正考验硬件功底的地方。记住今天的内容:规格化数、非规格化数、特殊值,这三者的编码规则必须烂熟于心。不然等你流片回来发现NaN传播错了,那可就真叫天天不应了。