浮点运算
1. 浮点数概览
1.1. 为什么需要浮点数
整数类型(有符号和无符号)无法满足所有计算需求,特别是:
* 表示带小数的数字(数学上的实数),如 π (3.14159...)、e (2.71828...)。
* 表示绝对值非常大或非常小的数,超出了标准整数(如32位整数)的表示范围,例如纳秒级的时间 (1.0 x 10^-9
) 或一百年的秒数 (3.15576 x 10^9
)。
1.2. 科学记数法 (Scientific Notation)
一种表示数字的方法,尤其适用于非常大或非常小的数。
* 规格化 (Normalized) 数:在科学记数法中,如果一个数没有前导零,并且小数点左边只有一位非零整数,则称其为规格化数。
* 例如 (十进制):1.0 x 10^-9
是规格化的。
* 0.1 x 10^-8
或 10.0 x 10^-10
不是规格化的。
* 二进制科学记数法:同样适用于二进制数。
* 例如:1.0₂ x 2⁻¹
。
* 为了规格化二进制数,基数必须为2,以确保小数点左边只有一位非零数(即1)。
* 小数点称为 二进制小数点 (binary point)。
1.3. 浮点数 (Floating Point)
计算机中用于表示实数的近似值的一种方式,其二进制小数点的位置是不固定的(“浮动”的),因此得名。C语言中使用 float
(单精度) 和 double
(双精度) 表示。
* 表示格式 (二进制规格化):1.xxxxxxx₂ x 2^yyyy
(此处 yyyy
为十进制指数,计算机内部也用二进制表示指数)。
* 优点:
1. 简化数据交换:统一的表示格式。
2. 简化算术算法:算法设计相对规整。
3. 提高精度:通过消除前导零,有效位可以存储更多有意义的数字。
2. IEEE 754 浮点表示标准
浮点数的表示需要在 尾数 (Fraction/Mantissa) 的位宽和 指数 (Exponent) 的位宽之间进行权衡。 * 增加尾数位宽 -> 提高精度。 * 增加指数位宽 -> 增大表示范围。
IEEE 754 是目前通用的浮点数算术标准,定义了浮点数的格式、运算规则、舍入方式以及异常处理。
2.1. 基本构成
一个浮点数通常由三部分组成: 1. 符号位 (Sign, S):1位,表示正负 (0为正,1为负)。 2. 指数域 (Exponent, E):若干位,存储指数的偏置值。 3. 尾数域 (Fraction, F):若干位,存储小数点后的有效数字。
2.2. 单精度 (Single Precision) - 32位
- 格式:
- 符号位 (S): 1位 (第31位)
- 指数域 (E'): 8位 (第30-23位)
- 尾数域 (F): 23位 (第22-0位)
- 数值表示 (初步):
(-1)^S x F x 2^E
(此处的F和E与实际存储和计算方式有关,后续会精确化)
2.3. 双精度 (Double Precision) - 64位
- 格式:
- 符号位 (S): 1位
- 指数域 (E'): 11位
- 尾数域 (F): 52位 (分布在两个32位字中,第一个字的高20位,第二个字的全部32位)
- 优势:主要优势在于提供更多的有效位数(更高的精度),其次是更大的表示范围。
2.4. 规格化数的隐含位 (Hidden Bit)
为了最大化尾数域的精度,IEEE 754 标准对规格化二进制数采用了一个技巧:
* 规格化的二进制数小数点左边第一位总是 1
(例如 1.fraction₂
)。
* 这个 1
不实际存储在尾数域中,而是被隐含存在。
* 因此,单精度实际有 1 + 23 = 24
位有效尾数,双精度有 1 + 52 = 53
位有效尾数。
* 有效位数 (Significand): 指隐含的1加上实际存储的尾数域F。
2.5. 偏置指数 (Biased Exponent)
指数域存储的并非真实的指数值,而是一个偏置后的值。
* 原因:
1. 使得可以用无符号整数比较来直接比较浮点数的大小(对于正数,指数大的值也大)。
2. 避免为指数单独设置符号位,简化指数的表示和比较。
* 计算:实际指数 (E_actual) = 存储的指数 (E_stored) - 偏置值 (Bias)
* 偏置值 (Bias):
* 单精度:127
* 双精度:1023
* 注意:指数域全0和全1有特殊含义,不用于表示常规规格化数。
* 单精度:实际指数范围约 -126 到 +127。
* 双精度:实际指数范围约 -1022 到 +1023。
2.6. IEEE 754 浮点数的完整公式
对于规格化的浮点数:
数值 = (-1)^S × (1 + Fraction) × 2^(Exponent_stored - Bias)
其中 Fraction
是尾数域F解释为 0.f₁f₂f₃...
的值。
2.7. 特殊值的编码 (参照图 3-13)
指数 (E_stored) | 尾数 (F) | 单精度对象 | 双精度对象 | 含义 |
---|---|---|---|---|
0 | 0 | ±0 | ±0 | 表示精确的零 |
0 | 非0 | ±非规格化数 | ±非规格化数 | 用于表示非常接近于0的数 (Gradual Underflow) |
1 至 254 (单) | 任何值 | ±浮点数 | 规格化浮点数 | |
1 至 2046 (双) | 任何值 | ±浮点数 | 规格化浮点数 | |
255 (单) / 全1s | 0 | ±无穷 (Infinity) | ±无穷 (Infinity) | 例如,一个数除以0的结果 |
255 (单) / 全1s | 非0 | NaN (Not a Number) | NaN (Not a Number) | 表示无效操作的结果,如 0/0 或 ∞ - ∞ |
- 非规格化数 (Denormalized Numbers):当指数域为全0且尾数域非0时,隐含的
1.
不再存在,数值为(-1)^S × (0 + Fraction) × 2^(1 - Bias)
。这允许表示比最小规格化数更接近0的数,实现“逐步下溢”。 - NaN (Not a Number):允许程序推迟对无效操作的测试和决策。
2.8. 浮点数的比较
- 符号位在最高位,方便快速测试正负。
- 指数域在尾数域之前,对于同符号的数,指数大的值也大(得益于偏置指数)。
2.9. 上溢 (Overflow) 和下溢 (Underflow)
- 上溢:正指数太大,无法在指数域中表示(结果的绝对值过大)。
- 下溢:负指数的绝对值太大,无法在指数域中表示(结果的绝对值过小,接近于0)。
- 逐步下溢 (Gradual Underflow):通过非规格化数,使0和最小规格化数之间的过渡更平滑。
2.10. 示例:表示 -0.75 (单精度)
- 十进制转二进制:-0.75₁₀ = -3/4₁₀ = -11₂ / 2²₁₀ = -0.11₂
- 科学记数法:-0.11₂ × 2⁰
- 规格化:-1.1₂ × 2⁻¹
- 符号位 S = 1
- 实际指数 E_actual = -1
- 尾数部分 (小数点后) F_bits = 100...0 (23位) -> (1).1₂ 中的
1
- 计算偏置指数:E_stored = E_actual + Bias = -1 + 127 = 126₁₀
- 126₁₀ = 01111110₂ (8位)
- 组合:
- S:
1
- E_stored:
01111110
- F_bits:
10000000000000000000000
(取规格化后小数点后的部分) - 结果:
1 01111110 10000000000000000000000
- S:
2.11. 示例:二进制浮点数转十进制
给定单精度浮点数:1 10000001 01000000000000000000000
1. 解析:
* S = 1 (负数)
* E_stored = 10000001₂ = 129₁₀
* F_bits = 0100...0₂
2. 计算实际指数:E_actual = E_stored - Bias = 129 - 127 = 2
3. 组合尾数:(1 + Fraction) = 1 + 0.01₂ = 1 + (02⁻¹ + 12⁻²) = 1 + 0.25 = 1.25₁₀
4. 计算最终值:(-1)^S × (1 + Fraction) × 2^E_actual
= (-1)¹ × 1.25 × 2²
= -1 × 1.25 × 4 = -5.0₁₀
2.12. 精解与延伸
- IEEE 754-2008:更新版标准,增加了16位(半精度)和128位(四精度)格式,并支持十进制浮点运算。
- 早期非2基数系统:如IBM 360/370使用基数16,指数改变1,尾数移动4位,可能导致前导零较多,损失精度。现代IBM大型机已支持IEEE 754。
3. 浮点加法
以 9.999₁₀ × 10¹ + 1.610₁₀ × 10⁻¹
为例(假设4位十进制有效数,2位十进制指数)。
3.1. 步骤
- 对阶 (Align Exponents):
- 比较两个数的指数,找出指数较小的数。
- 将指数较小的数的尾数右移,每右移一位,其指数加1,直到两个数的指数相同。
1.610 × 10⁻¹
->0.1610 × 10⁰
->0.01610 × 10¹
。- 由于精度限制(4位),
0.01610
变为0.016
(假设直接截断或简单舍入,精确舍入见后文)。
- 尾数相加 (Add Significands):
- 将对阶后的两个尾数(现在包括隐含的1,如果是二进制)相加。
9.999 + 0.016 = 10.015
(此处仍是十进制例子)。- 和为
10.015 × 10¹
。
- 结果规格化 (Normalize Result):
- 检查和是否为规格化形式。如果不是,需要调整。
10.015 × 10¹
->1.0015 × 10²
(尾数右移1位,指数加1)。- 如果和导致前导零(例如一正一负相加),则需要左移尾数,每左移一位,指数减1。
- 检查上溢/下溢:确保新的指数在可表示范围内。
- 舍入 (Round Result):
- 根据IEEE 754的舍入规则,将结果舍入到目标精度。
1.0015 × 10²
舍入到4位有效数字,变为1.002 × 10²
(假设5向上舍入)。- 注意:舍入可能导致结果再次非规格化(如
1.9999
舍入成2.000
),此时需要返回步骤3重新规格化。
3.2. 二进制浮点加法示例:0.5₁₀ + (-0.4375₁₀)
(假设4位二进制精度)
0.5₁₀ = 1/2₁₀ = 0.1₂ = 1.000₂ × 2⁻¹
-
-0.4375₁₀ = -7/16₁₀ = -0.0111₂ = -1.110₂ × 2⁻²
-
对阶:
-1.110₂ × 2⁻²
的指数 (-2) 小于1.000₂ × 2⁻¹
的指数 (-1)。-1.110₂ × 2⁻²
->-0.111₂ × 2⁻¹
(右移1位,注意符号和有效位)。 - 尾数相加:
(1.000₂) + (-0.111₂)
,指数同为2⁻¹
。1.000 - 0.111 = 0.001
(二进制减法)。 结果为0.001₂ × 2⁻¹
。 - 结果规格化:
0.001₂ × 2⁻¹
= 0.010₂ × 2⁻²
(左移1位,指数-1)= 0.100₂ × 2⁻³
(左移1位,指数-1)= 1.000₂ × 2⁻⁴
(左移1位,指数-1)。 检查上溢/下溢:-4在单精度允许的实际指数范围 (-126至127) 内。 偏置指数为-4 + 127 = 123
。 - 舍入:
1.000₂ × 2⁻⁴
已经是4位精度,无需舍入。 结果:1.000₂ × 2⁻⁴ = 0.0001₂ = 1/16₁₀ = 0.0625₁₀
。
3.3. 浮点加法硬件结构 (图 3-15)
主要部件: * 指数比较器:比较指数,确定差值。 * 移位器:对指数较小的数的尾数进行右移。 * 尾数加法器 (ALU):执行尾数相加/减。 * 规格化逻辑:对结果进行左移或右移,调整指数。 * 舍入硬件:执行舍入操作。
4. 浮点乘法
以 1.110₁₀ × 10¹⁰ × 9.200₁₀ × 10⁻⁵
为例(假设4位十进制有效数,2位十进制指数)。
4.1. 步骤
- 指数相加 (Add Exponents):
- 将两个操作数的实际指数相加。
- 如果使用偏置指数:
New_E_stored = E1_stored + E2_stored - Bias
(因为两个偏置值相加了,所以要减去一个偏置值)。 - 例:实际指数
10 + (-5) = 5
。 - 例:偏置指数
(10+127) + (-5+127) - 127 = 137 + 122 - 127 = 132
。实际指数132 - 127 = 5
。
- 尾数相乘 (Multiply Significands):
- 将两个操作数的有效位数(包含隐含的1)相乘。
(1.110) × (9.200) = 10.212000
。- 积的尾数部分为
10.212
(假设保留必要位数)。
- 结果规格化 (Normalize Result):
- 检查乘积是否为规格化形式。对于乘法,结果的整数部分可能大于1 (如
1x.xxxxx
或0.1xxxxx
如果操作数本身非常小)。 10.212 × 10⁵
->1.0212 × 10⁶
(尾数右移1位,指数加1)。- 检查上溢/下溢。
- 检查乘积是否为规格化形式。对于乘法,结果的整数部分可能大于1 (如
- 舍入 (Round Result):
- 将规格化后的结果舍入到目标精度。
1.0212 × 10⁶
舍入到4位有效数,变为1.021 × 10⁶
(假设截断或简单舍入)。
- 确定符号 (Determine Sign):
- 如果两个操作数符号相同,结果为正。
- 如果两个操作数符号相异,结果为负。
S_result = S1 XOR S2
。
4.2. 二进制浮点乘法示例:0.5₁₀ × (-0.4375₁₀)
-
1.000₂ × 2⁻¹
和-1.110₂ × 2⁻²
-
指数相加:
- 实际指数:
(-1) + (-2) = -3
。 - 偏置指数:
(-1+127) + (-2+127) - 127 = 126 + 125 - 127 = 124
。 - 实际指数
124 - 127 = -3
。
- 实际指数:
- 尾数相乘:
(1.000₂) × (1.110₂)
(忽略符号暂时) ``` 1.000 x 1.110 ------- 0000 1000 1000 1000
1.110000₂
``
有效数乘积为
1.110000₂。
3. **结果规格化**:
1.110000₂ × 2⁻³已经是规格化形式 (小数点左边是1)。
检查上溢/下溢:-3在允许范围内。
4. **舍入**:保留4位有效数 (1. FFF),
1.110₂ × 2⁻³无需舍入。
5. **确定符号**:一个正数,一个负数,结果为负。
最终结果:
-1.110₂ × 2⁻³。
验证:
-1.110₂ × 2⁻³ = -(1 + 1/2 + 1/4) × 1/8 = -(1.75) × 0.125 = -0.21875₁₀。
0.5 × -0.4375 = -0.21875₁₀`。
5. MIPS 中的浮点指令
MIPS 架构为浮点运算提供了专门的指令集和寄存器。
5.1. 浮点寄存器
- 32个浮点寄存器:
$f0, $f1, ..., $f31
。 - 单精度:每个寄存器存储一个32位单精度浮点数。
- 双精度:使用一对偶数-奇数编号的单精度寄存器存储一个64位双精度数,并以偶数寄存器编号作为其名称 (例如
$f2
代表由$f2
和$f3
组成的双精度寄存器)。实际可用的双精度寄存器为$f0, $f2, ..., $f30
(共16个)。
5.2. 浮点指令示例
- 算术运算:
add.s rd, rs, rt
(单精度加法:$f[rd] = $f[rs] + $f[rt]
)add.d rd, rs, rt
(双精度加法)sub.s
,sub.d
(减法)mul.s
,mul.d
(乘法)div.s
,div.d
(除法)
- 数据传输:
lwc1 rt, offset(base)
(Load Word Coprocessor 1: 从内存加载单精度到$f[rt]
)swc1 rt, offset(base)
(Store Word Coprocessor 1: 将$f[rt]
存储单精度到内存)l.d rd, offset(base)
(Load Double: MIPS-32新增, 加载双精度到$f[rd]
)s.d rd, offset(base)
(Store Double: MIPS-32新增, 存储双精度从$f[rd]
)- 基址寄存器 (
base
) 仍使用整数寄存器。
- 比较与跳转:
c.x.s cc, rs, rt
(单精度比较, x可以是eq, ne, lt, le, gt, ge
, cc是条件码寄存器)c.x.d cc, rs, rt
(双精度比较)bc1t label
(Branch on Coprocessor 1 True: 如果浮点条件码为真则跳转)bc1f label
(Branch on Coprocessor 1 False)
5.3. 硬件/软件接口与设计考虑
- 独立浮点寄存器:
- 优点:增加了总寄存器数量而无需改变指令格式中用于寄存器寻址的位数;允许整数和浮点单元并行访问各自的寄存器堆,增加带宽;可以为浮点操作定制寄存器特性。
- 缺点:可能需要额外的指令在整数寄存器和浮点寄存器之间传递数据(虽然MIPS主要通过内存传递)。
- 协处理器 (Coprocessor):早期设计中,浮点单元(FPU)常作为可选的协处理器芯片(如MIPS的CP1)。
lwc1
的c1
即指协处理器1。现代处理器多已将FPU集成到主芯片。 - MIPS-32增强:“单精度配对 (paired single)”指令,一条指令对64位寄存器中的两个32位操作数并行执行浮点操作 (类似SIMD)。如
add.ps $f0, $f2, $f4
等价于add.s $f0, $f2, $f4
和add.s $f1, $f3, $f5
。
5.4. Java与浮点
- Java 的
float
和double
类型遵循 IEEE 754 标准。 - Java 不直接支持类似C的多维数组(固定维度),而是通过数组的数组实现,每个子数组可以有不同长度,因此涉及更多边界检查和引用检查。
6. 算术精确性
浮点数通常是实数的近似表示,因为在任意两个实数之间都有无穷多个实数,而计算机表示是有限的。
6.1. 舍入误差
由于表示的位数有限,运算结果需要舍入,这会引入舍入误差。
6.2. 保护位 (Guard Bit)、舍入位 (Round Bit)、粘贴位 (Sticky Bit)
为了提高舍入的精度,使其尽可能接近“先以无限精度计算,然后舍入一次”的效果,IEEE 754标准要求在中间计算过程中保留额外的精度位: * 保护位 (G):尾数最低有效位右边的第一位。 * 舍入位 (R):保护位右边的第一位。 * 粘贴位 (S):记录在舍入位右边是否曾出现过非零位。如果任何被右移掉的、在舍入位之后的位是1,则粘贴位置1。
这些位用于更精确地决定如何舍入。例如,区分 X.50...00
(GRS=100) 和 X.50...01
(GRS=101 或 GRS=111)。
6.3. 舍入模式 (IEEE 754)
IEEE 754 定义了四种舍入模式: 1. 向正无穷舍入 (Round towards +∞):总是向上舍入。 2. 向负无穷舍入 (Round towards -∞):总是向下舍入。 3. 向零舍入 (Round towards Zero / Truncate):简单截断,丢弃多余位。 4. 向最近的偶数舍入 (Round to Nearest, Ties to Even): * 如果超出部分恰好是0.5 (即G=1, R=0, S=0),则舍入结果使最低有效位为偶数。 * 例如:2.5 -> 2 (最低位0是偶数),3.5 -> 4 (最低位0是偶数)。 * 这是默认模式,也是Java唯一支持的模式。它能提供统计上更无偏的结果。
6.4. ULP (Unit in the Last Place)
衡量浮点数误差的单位,指有效数最低位上的一个单位。IEEE 754保证在没有上溢、下溢或无效操作的情况下,基本算术运算(+,-,*,/,√)的结果误差在0.5 ULP以内。
6.5. 混合乘加 (Fused Multiply-Add, FMA)
- 一条指令完成
a = a + (b × c)
或a = (a × b) + c
等操作。 - 关键优势:只在最终加法后进行一次舍入,而不是乘法后一次、加法后一次。
- 这可以提高精度并可能提升性能。
- 已加入修订的 IEEE 754-2008 标准。
7. 小结与重点
- 位模式的无定性:相同的二进制位串可以表示有符号整数、无符号整数、浮点数、指令等,其具体含义取决于指令如何解释和操作它。
- 有限精度:计算机算术与数学上的理想算术不同,数字的大小和精度都有限制,可能发生上溢、下溢,或只是近似表示。
-
数据类型与MIPS指令映射:
C/Java 类型 MIPS 数据传输 MIPS 整数运算 (部分) MIPS 浮点运算 (部分) int
lw
,sw
add
,sub
,mult
,div
,slt
etc.unsigned int
lw
,sw
addu
,subu
,multu
,divu
,sltu
etc.float
lwc1
,swc1
add.s
,sub.s
,mul.s
double
l.d
,s.d
(MIPS32)add.d
,sub.d
,mul.d
-
小测验解答思路 (针对16位浮点,5位指数):
- 假设类IEEE 754格式:1位符号,5位指数,10位尾数。
- 偏置 (Bias) = 2^(5-1) - 1 = 2⁴ - 1 = 15。
- 最小规格化指数_stored = 1,最大规格化指数_stored = 2⁵ - 2 = 30 (全0和全1为特殊用途)。
- 最小实际指数 = 1 - 15 = -14。
- 最大实际指数 = 30 - 15 = 15。
- 最小规格化数:
±1.0000000000₂ × 2⁻¹⁴
- 最大规格化数:
±1.1111111111₂ × 2¹⁵
- 还包括 ±0, ±∞, NaN。
- 所以答案是 3. ±1.0000000000 x 2⁻¹⁴ 到 ±1.1111111111 x 2¹⁵, ±0, ±∞, NaN (选项中尾数位数有误,应为10位)。