浮点数表示与浮点运算单元详解
1. 单精度编码
单精度浮点数有一些特定的编码,如下表所示:
| 值 | 符号位 | 指数位 | 尾数 |
| ---- | ---- | ---- | ---- |
| 正零 | 0 | 00000000 | 00000000000000000000000 |
| 负零 | 1 | 00000000 | 00000000000000000000000 |
| 正无穷 | 0 | 11111111 | 00000000000000000000000 |
| 负无穷 | 1 | 11111111 | 00000000000000000000000 |
| QNaN | x | 11111111 | 1xxxxxxxxxxxxxxxxxxxxxx |
| SNaN | x | 11111111 | 0xxxxxxxxxxxxxxxxxxxxxxa |
其中,SNaN尾数域以0开头,但其余位至少有一个为1。
2. 十进制分数转换为二进制实数
当十进制分数可以表示为(1/2 + 1/4 + 1/8 + …)形式的分数之和时,很容易找到对应的二进制实数。例如:
| 十进制分数 | 分解形式 | 二进制实数 |
| ---- | ---- | ---- |
| 1/2 | 1/2 |.1 |
| 1/4 | 1/4 |.01 |
| 3/4 | 1/2 + 1/4 |.11 |
| 1/8 | 1/8 |.001 |
| 7/8 | 1/2 + 1/4 + 1/8 |.111 |
| 3/8 | 1/4 + 1/8 |.011 |
| 1/16 | 1/16 |.0001 |
| 3/16 | 1/8 + 1/16 |.0011 |
| 5/16 | 1/4 + 1/16 |.0101 |
然而,许多实数,如1/10 (0.1) 或1/100 (.01),不能用有限数量的二进制数字表示,只能用分母为2的幂的分数之和来近似。
二进制长除法转换方法
当涉及小的十进制值时,将十进制分数转换为二进制的一种简单方法是先将分子和分母转换为二进制,然后进行长除法。
例如,将十进制0.5转换为二进制:
十进制5是二进制0101,十进制10是二进制1010。进行二进制长除法:
1010
O1O1M
—1010
0
当从被除数中减去二进制1010时,余数为零,除法停止。因此,十进制分数5/10等于二进制0.1。
再如,将十进制0.2 (2/10) 转换为二进制:
.001 10011 (etc.)
1010
10.00000000
1010
1100
1010
10000
1010
1100
1010
etc.
第一个足够大的商是10000。将1010除入10000后,余数是110。添加另一个零后,新的被除数是1100。将1010除入1100后,余数是10。添加三个零后,新的被除数是10000,与开始时的被除数相同。从这一点开始,商中的位序列重复(0011…),所以我们知道无法找到精确的商,0.2不能用有限数量的位表示。单精度编码的尾数是00110011001100110011001。
3. 单精度值转换为十进制
将IEEE单精度(SP)值转换为十进制的步骤如下:
1. 如果最高有效位(MSB)为1,则该数为负数;否则为正数。
2. 接下来的8位表示指数。减去二进制01111111 (十进制127),得到无偏指数,并将其转换为十进制。
3. 接下来的23位表示尾数。记为“1.”,后面跟着尾数的位,可忽略尾随零。使用尾数、步骤1中确定的符号和步骤2中计算的指数,创建一个浮点二进制数。
4. 对步骤3中得到的二进制数进行非规范化处理(根据指数的值移动二进制点,指数为正时右移,为负时左移)。
5. 从左到右,使用加权位置表示法,形成由浮点二进制数表示的2的幂的十进制和。
示例
:将IEEE (0 10000010 01011000000000000000000) 转换为十进制
1. 该数为正数。
2. 无偏指数是二进制00000011,即十进制3。
3. 结合符号、指数和尾数,二进制数是 + 1.01011 X 2³。
4. 非规范化后的二进制数是 + 1010.11。
5. 十进制值是 + 10 3/4,即 + 10.75。
4. 浮点运算单元(FPU)
早期的Intel 8086处理器只能处理整数运算,这对于使用浮点计算的图形和计算密集型软件来说是个问题。虽然可以通过软件模拟浮点运算,但性能损失严重。后来,Intel推出了单独的浮点协处理器芯片8087,并随着处理器的升级不断改进。到了Intel 486,浮点硬件被集成到主CPU中,称为浮点运算单元(FPU)。
4.1 FPU寄存器栈
FPU不使用通用寄存器(如EAX、EBX等),而是有自己的一组寄存器,称为寄存器栈。它将值从内存加载到寄存器栈中,进行计算,并将栈中的值存储到内存中。FPU指令以后缀形式计算数学表达式,类似于惠普计算器。
例如,中缀表达式 (5 * 6) + 4 的后缀形式是 5 6 * 4 +;中缀表达式 (A + B) * C 的后缀形式是 A B + C *。
下面是一些中缀表达式和后缀表达式的示例:
| 中缀表达式 | 后缀表达式 |
| ---- | ---- |
| A + B | A B + |
| (A - B) / D | A B - D / |
| (A + B) * (C + D) | A B + C D + * |
| ((A + 8) / C) * (E - F) | A 8 + C / E F - * |
FPU有八个可单独寻址的80位数据寄存器,命名为R0到R7,它们共同构成一个寄存器栈。FPU状态字中的一个三位字段TOP标识当前栈顶的寄存器编号。例如,当TOP等于二进制011时,R3是栈顶,在编写浮点指令时,这个栈位置也称为ST(0)(或简称为ST),最后一个寄存器是ST(7)。
栈操作如下:
- 压入操作(也称为加载)将TOP减1,并将操作数复制到标识为ST(0)的寄存器中。如果压入前TOP等于0,则TOP环绕到寄存器R7。
- 弹出操作(也称为存储)将ST(0)处的数据复制到操作数中,然后将TOP加1。如果弹出前TOP等于7,则TOP环绕到寄存器R0。如果将值加载到栈中会覆盖寄存器栈中的现有数据,则会生成浮点异常。
下面是FPU栈在压入1.0和2.0后的变化:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([初始栈]):::startend --> B(压入1.0):::process
B --> C([栈中有1.0]):::startend
C --> D(压入2.0):::process
D --> E([栈中有2.0和1.0]):::startend
4.2 舍入
FPU试图从浮点计算中生成无限精确的结果,但在很多情况下,由于目标操作数可能无法准确表示计算结果,需要进行舍入。
例如,假设某种存储格式只允许三位小数位,对于精确结果 + 1.0111 (十进制1.4375),可以通过加.0001将数字向上舍入到下一个更高的值,或者通过减.0001向下舍入:
- (a) 1.0111 –> 1.100
- (b) 1.0111 –> 1.011
如果精确结果为负,加 - .0001会使舍入结果更接近 - ∞;减 - .0001会使舍入结果更接近零和 + ∞:
- (a) -1.0111 –> -1.100
- (b) -1.0111 –> -1.011
FPU允许选择四种舍入方法:
- 舍入到最接近的偶数:舍入结果最接近无限精确的结果。如果两个值同样接近,则结果为偶数(最低有效位 = 0)。
- 向下舍入到 - ∞:舍入结果小于或等于无限精确的结果。
- 向上舍入到 + ∞:舍入结果大于或等于无限精确的结果。
- 向零舍入(截断):舍入结果的绝对值小于或等于无限精确的结果。
FPU控制字中有两位名为RC字段,用于指定使用哪种舍入方法:
- 二进制00:舍入到最接近的偶数(默认)。
- 二进制01:向下舍入到负无穷。
- 二进制10:向上舍入到正无穷。
- 二进制11:向零舍入(截断)。
以下是对二进制 + 1.0111和 - 1.0111应用四种舍入方法的示例:
| 舍入方法 | 精确结果 | 舍入后结果 |
| ---- | ---- | ---- |
| 舍入到最接近的偶数 | +1.0111 | 1.100 |
| 向下舍入到 - ∞ | +1.0111 | 1.011 |
| 向上舍入到 + ∞ | +1.0111 | 1.100 |
| 向零舍入 | +1.0111 | 1.011 |
| 舍入到最接近的偶数 | -1.0111 | -1.100 |
| 向下舍入到 - ∞ | -1.0111 | -1.100 |
| 向上舍入到 + ∞ | -1.0111 | -1.011 |
| 向零舍入 | -1.0111 | -1.011 |
4.3 浮点异常
FPU识别和检测六种类型的异常情况:无效操作(#I)、除零(#Z)、非规范化操作数(#D)、数值溢出(#O)、数值下溢(#U)和不精确精度(#P)。前三种(#I, #Z, #D)在任何算术运算之前检测,后三种(#O, #U, #P)在运算之后检测。
每种异常类型都有对应的标志位和掩码位。当检测到浮点异常时,处理器会设置匹配的标志位。对于处理器标记的每个异常,有两种处理方式:
- 如果相应的掩码位被设置,处理器会自动处理异常,让程序继续运行。
- 如果相应的掩码位被清除,处理器会调用软件异常处理程序。
4.4 浮点指令集
FPU指令集较为复杂,主要包括以下基本类别:
- 数据传输
- 基本算术
- 比较
- 超越函数
- 加载常量(仅特殊预定义常量)
- x87 FPU控制
- x87 FPU和SIMD状态管理
浮点指令名以字母F开头,以区别于CPU指令。指令助记符的第二个字母(通常是B或I)表示如何解释内存操作数:B表示二进制编码的十进制(BCD)操作数,I表示二进制整数操作数。如果未指定,则内存操作数假定为实数格式。例如,FBLD操作BCD数,FILD操作整数,FLD操作实数。
FPU指令的操作数情况如下:
- 浮点指令可以有零个、一个或两个操作数。如果有两个操作数,其中一个必须是浮点寄存器。
- 没有立即操作数,但某些预定义常量(如0.0、π和log₂10)可以加载到栈中。
- 通用寄存器(如EAX、EBX、ECX和EDX)不能作为操作数(唯一的例外是FSTSW,它将FPU状态字存储在AX中)。
- 不允许内存到内存的操作。
- 整数操作数必须从内存加载到FPU中(绝不能从CPU寄存器加载),它们会自动转换为浮点格式。同样,当将浮点值存储到整数内存操作数中时,值会自动截断或舍入为整数。
4.5 初始化
FINIT指令初始化浮点运算单元。它将FPU控制字设置为037Fh,这会屏蔽(隐藏)所有浮点异常,将舍入设置为最接近,将计算精度设置为64位。建议在程序开始时调用FINIT,以便了解处理器的初始状态。
4.6 浮点数据类型
MASM支持的浮点数据类型有QWORD、TBYTE、REAL4、REAL8和REAL10,如下表所示:
| 类型 | 用途 |
| ---- | ---- |
| QWORD | 64位整数 |
| TBYTE | 80位(10字节)整数 |
| REAL4 | 32位(4字节)IEEE短实数 |
| REAL8 | 64位(8字节)IEEE长实数 |
| REAL10 | 80位(10字节)IEEE扩展实数 |
例如,当将浮点变量加载到FPU栈中时,变量可以定义为REAL4、REAL8或REAL10:
data
biqVal REAL1O 1.212342342234234243E+864
code
Rd biqVal ; 加载变量到栈中
4.7 加载浮点值(FLD)
FLD指令将浮点操作数复制到FPU栈的顶部(即ST(0))。操作数可以是32位、64位或80位的内存操作数(REAL4、REAL8、REAL10)或另一个FPU寄存器:
FLD m32fp
FLD m64fp
FLD m8Ofp
FLD ST(i)
FLD支持的内存操作数类型与MOV相同,以下是一些示例:
.data
array REAL8 10 DUP(?)
.code
fld array ; 直接
fld [array+16] ; 直接偏移
fld REAL8 PTR[esi] ; 间接
fld array[esi] ; 索引
fld array[esi*8] ; 索引,缩放
fld array[esi*TYPE array] ; 索引,缩放
fld REAL8 PTR[ebx+esi] ; 基址索引
fld array[ebx+esi] ; 基址索引偏移
fld array[ebx+esi*TYPE array] ; 基址索引偏移,缩放
下面是一个加载两个直接操作数到FPU栈的示例:
data
dblone REAL8 234.56
dblTwo REAL8 10.1
code
fld dblone ; ST(0) = dblone
fld dblTwo ; ST(0) = dblTwo, ST(1) = dblone
执行每个指令后,栈的内容如下:
fld dblOne
ST(0) 234.56
fld dblTwo
ST(1) 234.56
ST(0) 10.1
当第二条FLD指令执行时,TOP减1,之前标记为ST(0)的栈元素变为ST(1)。
4.8 加载整数(FILD)
FILD指令将16位、32位或64位有符号整数源操作数转换为双精度浮点数,并加载到ST(0)中,源操作数的符号会被保留。FILD支持的内存操作数类型与MOV相同(间接、索引、基址索引等)。
4.9 加载常量
以下指令用于将特殊常量加载到栈中,它们没有操作数:
- FLD1:将1.0压入寄存器栈。
- FLDL2T:将log₂10压入寄存器栈。
- FLDL2E:将log₂e压入寄存器栈。
- FLDPI:将π压入寄存器栈。
- FLDLG2:将log₁₀2压入寄存器栈。
- FLDLN2:将logₑ2压入寄存器栈。
- FLDZ:将0.0压入FPU栈。
4.10 存储浮点值(FST, FSTP)
FST指令将浮点操作数从FPU栈的顶部复制到内存中。FST支持的内存操作数类型与FLD相同。操作数可以是32位、64位或80位的内存操作数(REAL4、REAL8、REAL10)或另一个FPU寄存器:
FST m32fp
FST m64fp
FST ST(i)
综上所述,浮点数的表示和浮点运算单元在计算机的数值计算中起着至关重要的作用,了解它们的原理和使用方法对于开发高性能的计算程序非常有帮助。
5. 常见问题解答
在处理浮点数和使用浮点运算单元时,可能会遇到一些常见问题,下面为大家解答:
1.
为什么单精度实数格式不允许指数为 -127?
:单精度实数格式中,指数部分采用偏移量表示,偏移值为 127。如果指数为 -127,经过偏移后指数位全为 0,这种情况被用于表示零和非规范化数,并非正常的指数表示。
2.
为什么单精度实数格式不允许指数为 +128?
:指数位全为 1 时,有特殊用途,用于表示无穷大、NaN 等情况,所以不用于正常的指数表示,也就不允许指数为 +128。
3.
在 IEEE 双精度格式中,有多少位用于尾数的小数部分?
:IEEE 双精度格式中,尾数部分共 52 位用于表示小数部分。
4.
在 IEEE 单精度格式中,有多少位用于指数?
:IEEE 单精度格式中,有 8 位用于表示指数。
5.
如何将二进制浮点值 1101.01101 表示为十进制分数之和?
:
- 整数部分:$1101 = 1\times2^3 + 1\times2^2 + 0\times2^1 + 1\times2^0 = 8 + 4 + 0 + 1 = 13$
- 小数部分:$0.01101 = 0\times2^{-1} + 1\times2^{-2} + 1\times2^{-3} + 0\times2^{-4} + 1\times2^{-5} = 0 + \frac{1}{4} + \frac{1}{8} + 0 + \frac{1}{32} = \frac{8 + 4 + 1}{32} = \frac{13}{32}$
- 所以,$1101.01101 = 13 + \frac{13}{32} = \frac{416 + 13}{32} = \frac{429}{32}$
6.
为什么十进制 0.2 不能用有限数量的位精确表示?
:因为 0.2 不能表示为分母是 2 的幂的分数之和,在二进制转换中,其小数部分会无限循环,所以不能用有限数量的位精确表示。
7.
如何对二进制值 11011.01011 进行规范化?
:规范化的目标是将二进制数表示为 $1.xxx\times2^n$ 的形式。
- 对于 $11011.01011$,将小数点左移 4 位得到 $1.101101011\times2^4$。
8.
如何对二进制值 0000100111101.1 进行规范化?
:同样将小数点左移 11 位,得到 $1.001111011\times2^{11}$。
9.
如何显示二进制 + 1110.011 的 IEEE 单精度编码?
:
- 步骤如下:
1. 规范化:$1110.011 = 1.110011\times2^3$
2. 符号位:正数,符号位为 0。
3. 指数位:指数为 3,偏移 127 后为 $3 + 127 = 130$,二进制表示为 10000010。
4. 尾数位:去掉整数位的 1,尾数为 110011,后面补零至 23 位,即 11001100000000000000000。
5. 所以,IEEE 单精度编码为 0 10000010 11001100000000000000000。
10.
NaN 有哪两种类型?
:NaN 有 QNaN(Quiet NaN)和 SNaN(Signaling NaN)两种类型。
11.
如何将分数 5/8 转换为二进制实数?
:
- $5/8 = 1/2 + 0/4 + 1/8$,所以二进制实数为.101。
12.
如何将分数 17/32 转换为二进制实数?
:
- $17/32 = 1/2 + 0/4 + 0/8 + 0/16 + 1/32$,所以二进制实数为.10001。
13.
如何将十进制值 + 10.75 转换为 IEEE 单精度实数?
:
- 步骤如下:
1. 转换为二进制:$10.75 = 1010.11 = 1.01011\times2^3$
2. 符号位:正数,符号位为 0。
3. 指数位:指数为 3,偏移 127 后为 $3 + 127 = 130$,二进制表示为 10000010。
4. 尾数位:去掉整数位的 1,尾数为 01011,后面补零至 23 位,即 01011000000000000000000。
5. 所以,IEEE 单精度编码为 0 10000010 01011000000000000000000。
14.
如何将十进制值 -76.0625 转换为 IEEE 单精度实数?
:
- 步骤如下:
1. 转换为二进制:$76.0625 = 1001100.0001 = 1.0011000001\times2^6$
2. 符号位:负数,符号位为 1。
3. 指数位:指数为 6,偏移 127 后为 $6 + 127 = 133$,二进制表示为 10000101。
4. 尾数位:去掉整数位的 1,尾数为 0011000001,后面补零至 23 位,即 00110000010000000000000。
5. 所以,IEEE 单精度编码为 1 10000101 00110000010000000000000。
6. 总结
浮点数的表示和浮点运算单元在计算机科学中是非常重要的概念,它们为计算机处理实数提供了基础。通过本文,我们了解了单精度浮点数的编码规则,包括正零、负零、正无穷、负无穷、QNaN 和 SNaN 的表示。学会了将十进制分数转换为二进制实数的方法,以及将 IEEE 单精度值转换为十进制的步骤。
浮点运算单元(FPU)的出现解决了早期处理器只能处理整数运算的问题,提高了计算机在图形和计算密集型软件中的性能。FPU 有自己的寄存器栈,采用后缀形式计算数学表达式,并且支持多种浮点数据类型和指令集。
在使用 FPU 时,我们需要注意舍入方法、浮点异常的处理以及指令的操作数规则。合理使用 FPU 的初始化指令 FINIT 可以确保程序在一个已知的状态下开始运行。
总之,掌握浮点数的表示和 FPU 的使用对于开发高性能的计算程序至关重要,希望本文能帮助读者更好地理解和应用这些知识。
下面是一个总结 FPU 相关操作的表格:
| 操作 | 指令 | 说明 |
| ---- | ---- | ---- |
| 初始化 | FINIT | 将 FPU 控制字设置为 037Fh,屏蔽所有浮点异常,设置舍入为最接近,精度为 64 位 |
| 加载浮点值 | FLD | 将浮点操作数复制到 FPU 栈顶 |
| 加载整数 | FILD | 将整数转换为双精度浮点数并加载到 FPU 栈顶 |
| 加载常量 | FLD1、FLDL2T 等 | 将特殊常量加载到 FPU 栈中 |
| 存储浮点值 | FST、FSTP | 将 FPU 栈顶的浮点值复制到内存或其他寄存器 |
同时,为了更清晰地展示 FPU 指令的执行流程,我们给出以下 mermaid 流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(初始化 FPU: FINIT):::process
B --> C{选择操作}:::decision
C -->|加载浮点值| D(FLD 操作数):::process
C -->|加载整数| E(FILD 操作数):::process
C -->|加载常量| F(选择常量指令):::process
C -->|存储浮点值| G(FST 或 FSTP 操作数):::process
D --> H(执行计算):::process
E --> H
F --> H
G --> H
H --> I{是否有异常}:::decision
I -->|是| J(处理异常):::process
I -->|否| K(结束):::startend
J --> K
通过这个流程图,我们可以更直观地看到 FPU 指令的执行过程,以及异常处理的流程。希望这些内容能帮助读者更好地理解和运用浮点数和 FPU 的相关知识。
超级会员免费看
549

被折叠的 条评论
为什么被折叠?



