8.2.2 状态寄存器
x87 FPU有一个16位寄存器显示当前的状态:空闲标志(1位)、条件判断标志(4位)、数据寄存器堆栈指针(3位)、异常标志(6位)、全局错误标志(1位)、堆栈错误标志(1位)。如图8-3所示:
一般在一个操作完成以后,通过检测状态寄存器控制流程。在一般编程(www.cppentry.com)中,异常标志和条件标志是最重要的。其他部分仅在出现错误时才需要关注,详情请参阅文献[2]。
(1)指令
状态寄存器是只读的,因此与它有关的指令只有一条(但有同步和不同步两个版本,参见8.3节),即FSTSW。这条指令将状态寄存器中的内容传送至AX寄存器,位次序不变。由于状态寄存器共16位,因此正好填满AX。通过检测AX的对应位,即可得到状态控制器的相关信息,例如前面检测条件位C0的代码:
- FSTSW ;传送至AX
- AND EAX,100h ;测试C0=1
- JNZ MYNEXT2 ;ST0 < ST1
(2)堆栈指针
位11至位13是堆栈指针(共3位),其值在0和7之间循环。
与常规x86指令使用寄存器模式不同,浮点指令使用堆栈模式(更确切地说,是寄存器模式和堆栈模式的混合),即一般的浮点指令从栈顶取操作数,结果也存储于栈顶,例如FADDP指令就是如此,它的操作数来自ST(0)和ST(1),结果存储于ST(0)。
一般情形下,用户只需保证堆栈不出现错误(例如溢出),无需特别在意它的指针。但在特殊情形下,移动堆栈指针可能有利于提高效率。例如正在进行一个复杂的计算,中间结果占据了多个数据寄存器,如图8-4所示。
假设当前的堆栈指针位于data5处,而需要计算sin(data2),那么如何实现呢?常规方法(不直接操作堆栈指针)代码如下:
- FSTP data5
- FSTP data4
- FSTP data3
- FSIN data2
- FLD data3
- FLD data4
- FLD data5
如果直接操作堆栈指针(使用FINCSTP指令),那么代码如下:
- FINCSTP
- FINCSTP
- FINCSTP
- FSIN
- FDECSTP
- FDECSTP
- FDECSTP
可以看出,在复杂计算中,常规方法的代价就是在数据寄存器和内存之间进行多次数据传送,降低了执行效率,而直接操作堆栈指针可以避免这些问题。当然,这个例子是编造的,实际代码可能没有这么极端,而且操作堆栈指针相当危险(清空堆栈时很容易造成堆栈溢出)。除非迫不得已,一般不建议这么做。
(3)异常位
第5章已经提及IEEE定义了5种浮点异常,每种异常均对应一个位,共5个位。除此而外,还有一个弱规范数异常,当操作数中出现弱规范数时,这个异常位就会被设置(但扩展双精度格式例外)。
在这些硬件基础上,可以建立两种异常处理模式:正常模式和安静模式。
正常的异常处理是硬件触发异常,被内核(操作系统代码)捕获,然后由内核传递至用户层,交由指定的异常代码处理。这种处理模式的特点是:当一切正常、没有异常触发时,异常处理代码不会被激活,如同不存在一样,因此任务代码执行效率较高;但是一旦触发了异常,内核代码和用户代码均被激活,处理代码的执行效率较低,系统受到较大干扰。
安静模式就是屏蔽一切异常,但在任务代码执行过程中,在一些关键处(例如结果出来时)进行异常检测。如果检测到异常就进行异常处理,否则继续执行任务代码。这种模式即使在异常出现时也不会激活内核代码,对系统干扰较小,异常处理代码的效率也高。但是,大量的检测代码即使在没有异常时也需要运行(否则不知道是否发生了异常),从而导致任务代码效率低下。
两种模式各有优缺点,选择的关键在于异常被触发的频率。如果频率较低,那么正常模式有优势;反之,安静模式有优势。幸运的是,在浮点运算中,这两种模式都可以根据情形选用。Windows标准的异常处理模式就是正常模式,常见的try-except块和try-catch块就是这种模式在C/C++(www.cppentry.com)中的对应物。与此同时,VC6的浮点数学库使用安静模式处理数学函数内部可能出现的异常,例如,如果想处理asin()可能出现的定义域错误(非法操作异常的一种情形),那么只需提供一个_matherr()即可。不过这种模式仅用于VC6的数学函数库,对普通的浮点代码无效(VC6浮点库没有提供异常检测函数)。
关于异常处理以及使用参见第12章。
(4)条件位
状态寄存器有4个条件位,虽然最常见的用处是给出逻辑比较的结果,但这只是它们在逻辑比较指令中的作用,在别的指令中,它们还有其他作用,例如在FXAM指令中它们给出操作数的类型、在FPREM指令中它们返回商的最低3个位。考虑到这些,更确切的名字应该是指示位。
按返回结果方式的不同,x87 FPU有两类逻辑比较指令:一类指令直接将结果设置到EFLAGS寄存器中,例如FCOM指令;一类则将结果设置在状态寄存器的条件位中,例如FCOMI指令。两者在使用上也有差异,例如比较两个数的大小,使用FCOMI指令是:
- FLD QWORD PTR[ESI]
- FLD QWORD PTR[ESI+8]
- FCOMI ST(0), ST(1) ;占用EFLAGS部分
- JB MYNEXT2
需要将两个数都载入寄存器,但检测结果比较方便。而FCOM指令正好相反:
- FLD QWORD PTR[ESI]
- FCOM QWORD PTR[ESI+8]
- FSTSW ;占用AX
- AND EAX,100h ;测试C0=1
- JNZ MYNEXT2 ;ST0 < ST1
只需载入一个即可,但检测结果比较麻烦,而且会破环AX。
我喜欢使用FCOMI指令(此时无需关心C0,C1,C2,C3的意义),但在VC6浮点库中没有见到FCOMI指令。这里有一个重要的原因,那就是FCOMI指令是Pentium 6系列才引入IA-32体系的,先前的CPU不支持。因此,如果需要考虑兼容性,FCOM指令是唯一的选择。C0,C1,C2,C3的意义参见附录B指令说明部分。
另一个需要特别注意的细节是FCOMI指令的特性,它只设置EFLAGS寄存器的ZF、CF和PF,没有设置SF和OF,因此FCOMI指令的结果类似无符号整型的结果,这意味着紧跟的分支指令应该使用JA/JAE/JE/JBE/JB,而不要使用JG/JGE/JE/JLE/JL。否则,结果会出错。
【责任编辑:
董书 TEL:(010)68476606】