FPU环境
FPU寄存器堆栈
浮点运算器(floating point unit , 缩写 FPU)是一个自持的单元,它使用与标准处理器寄存器分离的一组寄存器处理浮点操作。附加的FPU寄存器包括8个80位数据寄存器和3个16位寄存器,称为控制(control) 、状态(status) 和标记(tag) 寄存器。FPU数据寄存器称为R0 到R7。
它们的操作和标准寄存器有些不同,不同之处在于它们连接在一起形成一个堆栈。和内存中的堆栈不同,FPU寄存器堆栈是循环的-_-这就是说,堆栈中的最后一个寄存器连接回堆栈中的第一个寄存器。
堆栈顶部的寄存器是在FPU的控制字寄存器中定义的,名为ST(0) 。除了顶部寄存器外的其他寄存器名称是ST(x) ,其中x可以是1 到7。
当数据被加载到FPU堆栈时,堆栈顶部沿着8个寄存器向下移动。当8个值被加载到堆栈中之后,所有8个FPU数据寄存器就都被使用了。如果把第9个数据加载到堆栈中,堆栈指针回绕到第一个寄存器,井且使用新的值替换这个寄存器中的值,这会产生FPU异常错误。
因为FPU独立于主处理器,所以它一般不使用EFLAGS寄存器来表示结果和确定行为。FPU包含它自己的寄存器组来执行这些功能。状态寄存器、控制寄存器和标记寄存器用于存取FPU的特性和确定FPU的状态。
- 状态寄存器
状态寄存器表明FPU的操作情况。它包含在一个16位寄存器中,不同的位作为不同标志。下表介绍状态寄存器位。
状态位 | 描述 |
---|---|
0 | 非法操作异常标志 |
1 | 非规格化操作数异常标志 |
2 | 除数为零异常标志 |
3 | 溢出异常标志 |
4 | 下溢异常标志 |
5 | 精度异常标志 |
6 | 堆栈错误 |
7 | 错误汇总状态 |
8 | 条件代码位0 (C0) |
9 | 条件代码位I (C1) |
10 | 条件代码位2 (C2) |
11-13 | 堆栈顶部指针 |
14 | 条件代码位3 (C3) |
15 | FPU繁忙标志 |
4个条件代码位(8 、9 、10 和14 位) 一起使用,表示浮点操作结果的特定错误代码。它们经常和异常标志一起使用,表示特定的异常情况。
FPU 的前6位是异常标志。在处理的过程中,当发生浮点异常时FPU 设置它们。FPU 保持它们的设置状态,直到程序运行时清空它们。当检测到堆栈溢出或者下溢时(值对于80位堆栈寄存器过大或者过小),设置堆栈错误标志。
堆栈顶部位用于表示哪个FPU 数据寄存器被设置为STO寄存器。8 个寄存器中的任何一个都可以被指派为堆栈的顶端。每个后续的寄存器袚相应地设为ST(x) 寄存器。
把值加载到堆栈中时,在值被加载之前,TOS 值递减l 。这样,因为默认的TOS 值为0, 所以R7 寄存器是堆栈顶部值的默认位置( ST0 ) 。这可能引起混乱,但是不必担心—FPU 堆栈会处理所有这些管理事务。
- 控制寄存器
控制寄存器控制FPU 内的浮点功能。这里定义了相关设置,比如FPU用于计算浮点值的精度以及用于舍入浮点结果的方法。
控制寄存器使用一个16位寄存器,下表列出了相应位的含义。
控制位 | 描述 |
---|---|
0 | 非法操作异常掩码 |
1 | 非规格化操作数异常掩码 |
2 | 除数为零异常掩码 |
3 | 溢出异常掩码 |
4 | 下溢异常掩码 |
5 | 精度异常掩码 |
6-7 | 保留 |
8-9 | 精度控制 |
10-11 | 舍入控制 |
12 | 无穷大控制 |
13-15 | 保留 |
控制寄存器的前6位用于控制使用状态寄存器中的哪些异常标志。当这些位中的一位被设置时,就会防止状态寄存器中对应的异常标志被设置。默认情况下,所有掩码位都被设置,即屏蔽所有异常。
精度控制位可以设置FPU 中用于数学计算的浮点精度。这是非常有用的控制特性,可以改变FPU计算浮点值花费的时间。精度控制位可能的设置如下:
控制位 | 含义 |
---|---|
00 | 单精度(24位有效位) |
01 | 未使用 |
10 | 双精度(53位有效位) |
11 | 扩展双精度(64位有效位) |
默认情况下, FPU精度被设置为扩展双精度。这是最为精确的值,但是也最耗费时间。如果不打算使用这么高的精度,可以把这个值设置为单精度以便加快浮点值的计算速度。
类似地,舍入控制位可以设置FPU如何舍入浮点计算的结果。舍入控制位的可能设置如下:
控制位 | 含义 |
---|---|
00 | 舍入到最近值 |
01 | 向下舍入(向无穷大负值) |
10 | 向上舍入(向无穷大正值) |
11 | 向零舍入 |
默认情况下,舍入控制位被设置为舍入到最近值。
控制寄存器的默认值是0x037F 。可以使用FSTCW指令把控制寄存器的设置加载到双字内存位置中查看设置的内容。也可以使用FLDCW指令改变设置。这条指令把双字内存值加载到控制寄存器中
- 标记寄存器
标记寄存器用于标识8 个80位FPU 数据寄存器中的值。标记寄存器使用16位(每个寄存器2位)标识每个FPU数据寄存器的内容。
每个标记值对应一个物理的FPU寄存器。每个寄存器对应的2位值可以包含表明寄存器内容的4个特殊代码之一。在任何给定的时刻, FPU数据寄存器可以包含下面的内容:
标记代码 | 含义 |
---|---|
代码00 | 一个合法的扩展双精度值 |
代码01 | 零值 |
代码10 | 特殊的浮点值 |
代码11 | 无内容(空) |
这使开发者可以快速检查标记寄存器以便确定FPU寄存器中是否包含合法数据,而不必读取和分析寄存器的内容,虽然在实际操作中,因为是开发者把值压人到寄存器堆栈中的,所以开发者应该知道其中的内容是什么。
基本浮点运算
FPU提供对浮点值执行基本数学功能的指令。下表介绍这些基本功能。
指令 | 描述 |
---|---|
FADD | 浮点加法 |
FDIV | 浮点除法 |
FDIVR | 反向浮点除法 |
FMUL | 浮点乘法 |
FSUB | 浮点减法 |
FSUBR | 反向浮点减法 |
实际上,这些功能的每一个都具有单独的指令和格式,可以生成6个可能的功能,这取决于希望执行的确切操作是什么。例如, FADD指令可以像下面这样使用:
FADD source
: 内存中的32位或者64位值和ST0寄存器相加
FADD %st(x), %st(0)
: st(x) 和st(0)相加,结果存储到st(0) 中
FADD %s t(0), %st(x)
: st(0) 和st(x)相加,结果存储到st(x) 中
FADDP %st(0), %st(x)
: st(0) 和st(x)相加,结果存储到st(x) 中,并且弹出s t(0)
FADDP
: st(0) 和st(1)相加,结果存储到st(l) 中,并且弹出st(0)
FIADD source
: 16位或者32位整数值和st(0)相加,结果存储到st(0) 中
每种不同的格式指定操作中使用哪个FPU寄存器,以及操作完成后如何处理寄存器(要么保留,要么弹出堆栈) 。跟踪FPU寄存器值的状态很重要。
有时候,复杂的数学操作会执行多个操作,这些操作把各种值存储到不同的寄存器中,这时跟踪FPU寄存器值的状态可能是困难的工作。
对于GNU 汇编器,情况甚至变得更加复杂。指定内存中的值的指令的助记符必须包含一个字符的长度指示符(s用于32位单精度浮点值, l用于双精度浮点值)。而且,像以往一样,源和目标操作数的顺序和Intel语法中的顺序是相反的。
高级浮点运算
除了简单的加法、减法、乘法和除法之外,还有许多其他的浮点运算。FPU提供很多对浮点数执行的高级功能。如果为科学研究或者工程技术应用编写汇编语言程序,就很可能必须在程序中引入高级运算功能。
下表介绍可用的高级功能。
指令 | 描述 |
---|---|
F2XM1 | 计算2的乘方(次数为ST0中的值)减去1 |
FABS | 计算ST0 中的值的绝对值 |
FCHS | 改变ST0 中的值的符号 |
FCOS | 计箕ST0 中的值的余弦 |
FPATAN | 计算ST0 中的值的部分反正切 |
FPREM | 计算ST0 中的值除以ST1 中的值的部分余数 |
FPREM1 | 计箕ST0 中的值除以ST1 中的值的IEEE部分余数 |
FPTAN | 计箕ST0 中的值的部分正切 |
FRNDINT | 把ST0 中的值舍入到朵近的整数 |
FSCALE | 计算ST0 乘以2 的ST1 次乘方 |
FSIN | 计箕ST0 中的值的正弦 |
FSINCOS | 计算ST0 中的值的正弦和余弦 |
FSQRT | 计算ST0 中的值的平方根 |
FYL2X | 计算STl*log ST0 (以2为基数) |
FYL2XP1 | 计算ST1*log(ST0+ 1 ) (以2 为基数) |
- 部分余数
部分余数是浮点除法中难于处理的一个部分。部分余数的概念与如何执行浮点除法有关。除法操作的余数由被除数对除数的一系列减法决定。在每次减法迭代时,中间余数称为部分余数(partial remainder) 。当部分余数小于除数时(不能再执行减法操作,否则就会产生负数),迭代停止。在除法操作结束时,最终的答案是代表减法迭代次数的整数值(称为商(quotient)),以及表示最终的部分余数的浮点值(现在称为余数( remainder) ) 。
根据执行除法需要多少次迭代,可能有很多部分余数。迭代的次数取决于被除数和除数的指数值之间的差值。每次减法不能使被除数的指数值的减少最超过63。
FPREM和FPREM1 指令都计算浮点除法的余数值,但是它们的工作方法稍有区别。
确定除法余数的基本方法是确定被除数和除数的除法的浮点商,然后把这个值舍入到最近的整数。那么,余数就是除数和商相乘的结果与袚除数之间的差值。例如,为了计算20.65 除以3.97 的余数,可以执行如下步骤:
1) 20.65/3.97=5.201511335 , 舍入到5 (这是商)
2) 5*3.97 = 19 .85
3) 20.65- 19.85 = 0.8 (这是余数)
困难的部分在于舍入过程。在创建部分余数的任何标准之前, Intel就开发了FPREM指令。
Intel 的开发人员选择使用默认的FPU 向零舍人的方法,用于计算整数商值,然后确定余数。
不幸的是,当IEEE创建标准时,它选择在计算余数之前,使商值向上舍人到最近的整数值。
虽然这似乎只有细微的区别,但是在处理过程中计算部分余数时造成很大影响。出于这个原因,Intel 选择保持原始FPREM指令的原始形式,并且另外创建了FPREM1 指令,它使用IEEE方法计箕部分余数。
计算部分余数的问题在于必须知道迭代过程在什么时候完成。FPREM 和FPREM1 指令都使用FPU 状态寄存器的条件代码位2 (状态寄存器的第10位)表示迭代何时完成。当需要更多的迭代时,就设控C2位,当迭代完成时,就渚空C2位。
为了检查C2位,必须首先使用FSTSW指令把状态寄存器的内容复制到内存位置或者AX 寄存器中,然后使用TEST指令判断这一位是否被设置。
FPU的另一个巨大优势是计算三角函数的能力。一般的三角函数,比如正弦、余弦和正切,都可以轻松地从FPU获得。
FSIN和FCOS指令
在FPU 中,基本的三角函数都按照相同的方式实现。这些指令都使用一个隐含的源操作数,它位于ST0寄存器中。当函数完成时,结果存放在ST0寄存器中。
这些函数唯一难于处理的地方在于它们都使用弧度作为源操作数的单位。如果正在处理的应用程序使用角度,那么在能够使用FPU的三角函数之前,必须把值转换为弧度。完成这一转换的公式如下:
radians = (degrees * pi) / 180
在FPU中,这一计算很容易通过下面的代码片断完成:
fsts degree1 # load the degrees value stored in memory into ST0
fidivs val180 # divide by the 180 value stored in memory
fldpi # load pi into ST0, degree/180 now in ST1
fmul %st(l), %st(0) # multiply degree/180 and pi, saving in ST0
fsin # perforpi trig function on value in ST0
FPTAN 和FPATAN指令
FPTAN和FPATAN指令与它们对应的正弦和余弦指令有些不同。虽然它们计算正切和反正切三角函数,但是它们的输入和输出需求稍有不同。
FPTAN指令使用标准的位于ST0寄存器中的隐含操作数(同样,角的单位必须是弧度,不能是角度)。正如我们预期的,正切值被计算出来并且存放到ST0寄存器。
之后,值1.0被压入FPU堆栈,把正切值向下移动到ST1 寄存器。这样做的原因是为了向下兼容为80287 FPU协处理器编写的应用程序。在那个时期, FSIN和FCOS还不可用,计算这些值需要使用正切值的倒数。通过在FPTAN指令之后使用简单的FDIVR指令,就可以计算出余切值。
FPATAN指令使用两个隐含的源操作数。它计算角值ST1/ST0 的反正切值,并且把结果存放在ST1 中,然后弹出FPU堆栈,把值移动到ST0 。这种形式用于支持计算无穷比例—就是说,当ST0为零时的反正切。标准的ANSIC 函数atan2(double x, double y)也使用相同的概念。
对数函数
FPU的对数函数提供用于执行底数为2的对数计算的指令。
FYL2X指令执行如下计算:
ST(1) * log2 (ST(0))
FYL2X1 指令执行以下这样的计算:
ST(1) * log2 (ST(0) + 1.0)
FSCALE指令计算ST(0)乘以2 的ST(1)次乘方。它可以用于放大(通过在ST(1) 中使用正值),也可以用于缩小(通过在ST(1) 中使用负值)。
浮点条件分支
不幸的是,浮点数的比较不像整数的比较那么容易。在处理整数时,很容易使用CMP指令并且评估EFLAGS 寄存器中的值来确定值是大于、等于还是小于。
对于浮点数,不能奢望使用CMP指令。FPU提供一些它自己的指令来比较浮点值。
FCOM指令系列
FCOM指令系列用于在FPU 中比较两个浮点值。指令比较的一方是加载到FPU寄存器ST0 中的值,另一方要么是另一个FPU寄存器,要么是内存中的浮点值。还有在比较之后把一个值或者两个值弹出FPU堆栈的选项。下表介绍可以使用的不同指令版本。
指令 | 描述 |
---|---|
FCOM | 比较ST0寄存器和ST1 寄存器 |
FCOM ST(x) | 比较ST0寄存器和另一个FPU寄存器 |
FCOM source | 比较ST0寄存器和32位或者64位的内存值 |
FCOMP | 比较ST0寄存器和ST1 寄存器,井且弹出堆栈 |
FCOMP ST(x) | 比较ST0寄存器和另一个FPU寄存器,井且弹出堆栈 |
FCOMP source | 比较ST0寄存器和32位或者64位的内存值,并且弹出堆栈 |
FCOMPP | 比较ST0 寄存器和ST1 寄存器,并且两次弹出堆栈 |
FTST | 比较ST0寄存器和值0.0 |
比较的结果设置在状态寄存器的c0、C2和C3条件代码位中。比较可能产生的值列在下表中。
条件 | C0 | C2 | C3 |
---|---|---|---|
ST0 > source | 0 | 0 | 0 |
ST0 < source | 0 | 0 | 1 |
ST0 = source | 1 | 0 | 0 |
必须使用FSTSW指令把状态寄存器的值复制到AX寄存器或者内存位置中,然后使用TEST指令判断比较的结果。
关于相等性比较的一点说明:要记住,当把浮点值加载到FPU 寄存器中时,它被转换为扩展双精度浮点值。这一处理可能导致一些舍入错误。单精度或者双精度值在加载到FPU 寄存器之后有可能不等于原始值。检测浮点值的完全相等性不是个好主意,而要检测它们是否在预期值的小误差之内。
FCOMI指令系列
大家也许奇怪,既然在比较指令之后使用FSTSW和SAHF指令组合很有用,为什么不把它们合并成单一指令呢?回答是,确实这样做了。从奔腾Pro处理器系列开始, FCOMI指令可以完成这个任务。FCOMI指令系列执行浮点比较并且把结果存放到EFLAGS 寄存器中的进位、奇偶校
验和零标志。下表介绍FCOMI指令系列。
指令 | 描述 |
---|---|
FCOMI | 比较ST0寄存器和ST(x) 寄存器 |
FCOMIP | 比较ST0寄存器和ST(x) 寄存器,并且弹出堆栈 |
FUCOMJ | 在比较之前检查无序值 |
FUCOMJP | 在比较之前检查无序值,并且在比较之后弹出堆栈 |
从上表的描述可以看出, FCOMI指令系列的局限性在于它们只能比较FPU寄存器中的两个值,不能比较FPU 寄存器和内存中的值。
上表中的最后两条指令执行FCOM指令系列没有提供的功能。FUCOMI 和FUCOMIP指令确保被比较的值是合法的浮点数(使用FPU标记寄存器)。如果出现无序值,就会抛出异常。
FCOMI指令的输出使用EFLAGS 寄存器,如下表所示。
条件 | ZF | PF | CF |
---|---|---|---|
ST0 > ST(x) | 0 | 0 | 0 |
ST0 < ST(x) | 0 | 0 | 1 |
ST0 = ST(x) | 1 | 0 | 0 |
FCMOV指令系列
和用于整数的CMOV 指令类似, FCMOV指令可以编写浮点值的条件传送。根据EFLAGS 寄存器中的值, FCMOV 系列的指令把FPU寄存器ST(x) 中的源操作数传送到FPU 寄存器ST(0) 中的目标操作数。如果条件为true, 就把ST(x)寄存器中的值传送到ST(0)寄存器。
因为根据EFLAGS寄存器进行传送,所以更常见的方式是在FCMOV指令之前使用FCOMI指令。下表概述FCMOV 指令系列。
指令 | 描述 |
---|---|
FCMOVB | 如果ST (0) 小于ST (x) , 则进行传这 |
FCMOVE | 如果ST (0) 等干ST (x) , 则进行传送 |
FCMOVBE | 如果ST ( O) 小于或者等于ST (x) ,则进行传送 |
FCMOVU | 如果ST (0) 无序,则进行传送 |
FCMOVNB | 如果ST (0) 不小干ST (x) , 则进行传送 |
FCMOVNE | 如果ST ( O) 不等于ST (x) , 则进行传送 |
FCMOVNBE | 如果ST (0) 不小于或者等于ST (x) , 则进行传送 |
FCMOVNU | 如果ST (0) 非无序, 则进行传送 |
指令的GNU格式是:
fcmovxx source, destination
其中source是ST(x) 寄存器, destination是ST(0)寄存器。
保存和恢复FPU状态
不幸的是,在现在的IA-32 处理器中, FPU数据寄存器必须完成双重工作。MMX技术使用FPU数据寄存器作为MMX数据寄存器,存储80位打包整数值用于计算。如果在同一个程序中使用FPU和MMX功能,就有可能“破坏“数据寄存器。为了帮助防止这种情况, IA-32平台包含几个指令,它们可以保存FPU处理器的状态并且在其他处理完成之后恢复先前的状态。
保存和恢复FPU环境
FSTENV指令用千把FPU 的环境存储到一个内存块中。下面的FPU寄存器将被存储:
- 控制寄存器
- 状态寄存器
- 标记寄存器
- FPU指令指针偏移量
- FPU数据指针
- FPU最后执行的操作码
保存和恢复FPU 状态
FSTENV 指令存储FPU环境,但是FPU 中的数据没有被保存。为了保存包括数据在内的完整FPU环境,必须使用FSAVE指令。
FSAVE指令把所有FPU寄存器复制到一个108 字节的内存位置,然后初始化FPU状态。使用FRSTOR 指令恢复FPU 时,所有FPU 寄存器(包括数据寄存器)都被恢复为执行FSAVE指令时的状态。
等待和非等待指令
如果读者研究Intel 的手册,可能会注意到一些浮点指令有对应的非等待版本。术语”等待”和“ 非等待"涉及指令如何处理浮点异常。
浮点异常在前面的"状态寄存器”中讨论过。浮点指令可能生成6种浮点异常。它们通常表明运算过程中出现了某些错误(比如试图以零作为除数) 。
大多数浮点指令在执行之前必须等待以便确保前面的指令没有抛出异常。如果出现异常,在能够执行下一条指令之前必须先处理异常。
还有另一种方式, 一些指令包含非等待版本,它们不等待浮点异常的检查。这些指令允许程序保存或者复位当前的FPU 状态,而不处理任何悬而未决的异常。下表介绍可以使用的非等待指令。
指令 | 描述 |
---|---|
FNCLEX | 清空浮点异常标志 |
FNSAVE | 把FP U状态保存到内存中 |
FNSTCW | 保仵FPU控制寄存器 |
FNSTENV | 把FPU操作环境保存到内存中 |
FNSTSW | 把FPU状态寄存器保存到内存或者AX寄存器中 |
优化浮点运算
浮点运算可能是汇编语言应用程序中最为耗费时间的部分。一定要尝试优化浮点代码,尽可能地提高运算的性能。Intel提供了编写浮点程序的一些简单技巧:
- 确保浮点值不会上溢或者下溢出数据元素。
- 把精度控制位设置为单精度。
- 使用查找表实现简单的三角函数。
- 在可能的情况下, 断开依赖链。例如,不计算
z = a + b + c + d;
, 而是计算x = a + b; y = c + d; z = x + y;
- 在FPU寄存器中尽可能多地保留方程式的值。
- 在处理整数和浮点值时,把整数加载到FPU寄存器中并且执行运算,这样比对整数使用浮点指令要快。例如,不使用FIDIV, 而是使用FILD加载整数,然后对FPU寄存器中的值执行FDIVP指令。
- 尽可能使用FCOMI指令,不使用FCOM指令。
上述笔记内容学习自 AT&T Professional Assembly Language — Chapter IX