27. 特别主题
27.1 LEA指令(所有处理器)
在很多场合下,LEA指令都派得上用场,因为只靠这1条指令,就能在1个时钟内做1次移位、2个加法和1次数据传输。
比如:
LEA EAX,[EBX+8*ECX-1000]
比
MOV EAX,ECX / SHL EAX,3 / ADD
EAX,EBX / SUB EAX,1000快得多
LEA指令能在不改变标志寄存器的前提下做加法或移位,且源操作数和目的操作数不需要尺寸相同,因此可以考虑用LEA
EAX,[BX]取代MOVZX EAX,BX(尽管大多数处理器并没有对这种做法进行过优化)。
然而在PPlain和PMMX上,如果LEA指令使用了在前1个时钟周期被写过的基址或变址寄存器的话,它可能会遭遇AGI延迟。
在PPlain和PMMX上,因为LEA指令在u、v管道都能配对,而移位指令只能在u管道配对,因此如果你希望指令在v管道配对的话,可以用LEA取代移位位数为1、2或3的移位指令。
对于32位处理器,只含比例变址寄存器的寻址方式事实上不存在,因此像LEA
EAX,[EAX*2]实际编码的时候会变成带有4字节立即数偏移的指令LEA EAX,[EAX*2+00000000]。 故为了减小代码尺寸,你可以用LEA
EAX,[EAX+EAX]或更好的ADD EAX,EAX。 后者在PPlain和PMMX上是不可能AGI延迟的。
如果你正好有一个寄存器的值是0(比如循环结束后的循环计数器),那么你可以用它做基址寄存器来减小代码尺寸:
LEA EAX,[EBX*4] ; 7字节
LEA
EAX,[ECX+EBX*4] ; 3字节
27.2 除法(所有处理器)
除法相当耗费时间。
在PPro,PII和PIII上,对于除数是字节、字、双字的整型除法,耗费的时间分别是19,23,39个时钟周期。
在PPlain和PMMX上,无符号整型除法花的时间与上述差不多,带符号的花的时间要多一些。
因此在不会溢出的前提下,最好用小尺寸操作数(哪怕有操作数尺寸前缀的代价),尽可能地用无符号除法。
*除数是常量的整数除法(所有处理器)
除数是2的幂次的整型除法可以由右移实现。
除数是2^N的无符号整型除法:
SHR EAX,N
除数是2^N的有符号整型除法:
CDQ
AND EDX, (1 SHL N) -1 ; 或 SHR EDX, 32-N
ADD EAX, EDX
SAR EAX, N
如果N>7的话,上面的SHR指令比AND短。
然而SHR只能进入端口0执行(且只能在u-管道配对),AND能进端口0或1的任何一个(且在u、v管道都能配对)。
除数是常量的除法可以用乘以倒数来做。
为了计算无符号整型除法q=x/d,可以先计算除数的倒数f=2^r/d,这里的r定义了二进制小数点的位置(基点)。 然后把x与f相乘再右移r位即可。
r的最大值是32+b,b是d的有效二进制位的位数-1(即b是满足2^b<=d的最大整数)。 用了r=32+b就可以覆盖被除数x的最大范围。
为了弥补舍入造成的误差,上面的算法还需要做一些细分的操作。
因此下面的方法用了取整操作,得到了无符号整数除法的正确结果,它的结果与DIV指令给出的结果是一致的(感谢 Terje Mathisen 发明了这个方法):
b = (d的有效位位数) - 1
r = 32 + b
f = 2^r / d
如果f是一个整数,那么可以判断d是2的幂次:case A。
如果f不是整数,那么看f的小数部分是否 < 0.5 :f的小数部分 < 0.5,case
B;f的小数部分> 0.5 ,case C。
case A: (d = 2^b)
结果 = x
SHR b
case B: (f的小数部分 <
0.5)
把f向下取整,然后
结果 =
((x+1) * f) SHR r
case C: (f的小数部分 >
0.5)
把f向上取整,然后
结果 = (x *
f) SHR r
示例:
假定除数是5,
5 = 00000101b。
b = (有效位位数) - 1 = 2
r = 32+2 = 34
f = 2^34 / 5 = 3435973836.8 = 0CCCCCCCC.CCC...
(十六进制)
f的小数部分大于0。5,进入case C,f向上取整得到0CCCCCCCDh。
下面的代码计算EAX/5,用EDX返回结果:
MOV EDX,0CCCCCCCDh
MUL EDX
SHR EDX,2
在乘法之后,EDX中的数已经是积右移32位的结果了,因此对于r=34,只要再右移2位就可以了。
如果除数是10,只要把最后一条指令改成SHR EDX,3即可。
如果是B情形,代码为:
INC EAX
MOV EDX,f
MUL EDX
SHR EDX,b
除了x是0FFFFFFFFH,其它情况下都工作正常。
在EAX是0FFFFFFFFH时,因为INC指令将溢出,得到最终结果是0。 如果程序中x可能是0FFFFFFFFH,把代码改为:
MOV EDX,f
ADD EAX,1
JC DOVERFL
MUL EDX
DOVERFL:SHR
EDX,b
如果程序中x的取值范围是有限的,你可以将r取得小点,也就是更少的移位位数。 将r取得小基于以下原因:
*你可以令r=32,免除最后的SHR EDX,b指令
*你可以令r=16+b,这样乘法指令得到的结果就是32位而不是64位,就可以不用EDX寄存器了:
IMUL EAX,0CCCDh / SHR EAX,18
*你可以选择一个r值进入C情形而不是b情形,这样可以免除INC EAX指令。
在上述情况下,x的最大值至少是2^(r-b),有时更大。
如果想确切地知道使你的代码可以正确工作的x的最大值,你必须系统性地进行测试。
你还可以利用26.5章的方法,用更快的指令取代较慢的乘法指令。
下面的例子计算EAX/10,结果在EAX中返回。
我选择r=17而不是19,因为这样正好能给出一个较易优化的代码,而且与r=19能够覆盖的x的范围一致:
f = 2^17 / 10 = 3333h, case B: q = (x+1)*3333h:
LEA EBX,[EAX+2*EAX+3]
LEA
ECX,[EAX+2*EAX+3]
SHL EBX,4
MOV EAX,ECX
SHL ECX,8
ADD EAX,EBX
SHL EBX,8
ADD EAX,ECX
ADD EAX,EBX
SHR EAX,17
系统测试表明,对于所有x<10004H,这段代码能正确工作。
*除数是同一个值的多次整数除法(所有处理器)
如果在写汇编代码的时候不知道除数是多少,但是重复地使用同一个除数,那么也可以用上面的办法。
代码必须对A,B,C三种情况分别讨论,在做除法之前先计算f。
下面的代码显示了如何用同一个除数做多次除法(用了取整操作的无符号除法)。
先调用SET_DIVISOR确定除数并求出其倒数,然后对每个值调用DIVIDE_FIXED,做除数相同的除法。
.data
RECIPROCAL_DIVISOR DD ? ;
除数的倒数取整后的值
CORRECTION DD ? ; case A: -1, case B: 1, case
C: 0
BSHIFT DD ? ; 除数的有效二进位位数 - 1
.code
SET_DIVISOR PROC NEAR
; divisor in EAX
PUSH EBX
MOV EBX,EAX
BSR ECX,EAX
; b = 除数的有效二进位位数 - 1
MOV EDX,1
JZ ERROR ; 错误: 除数是0
SHL EDX,CL ; 2^b
MOV
[BSHIFT],ECX ; 保存b
CMP EAX,EDX
MOV EAX,0
JE SHORT CASE_A ;
除数是2的幂次
DIV EBX ; 2^(32+b) / d
SHR EBX,1 ; 除数 / 2
XOR
ECX,ECX
CMP EDX,EBX ; 把剩余值与divisor/2比较
SETBE CL ; 如果是case B,则1
MOV [CORRECTION],ECX ; 矫正取整误差
XOR ECX,1
ADD EAX,ECX ; 如果是case C,则加1
MOV [RECIPROCAL_DIVISOR],EAX ; 除数的倒数取整
POP EBX
RET
CASE_A: MOV [CORRECTION],-1 ; 记住我们在case A
POP EBX
RET
SET_DIVISOR ENDP
DIVIDE_FIXED PROC NEAR ;
被除数在EAX,返回结果也在EAX
MOV EDX,[CORRECTION]
MOV ECX,[BSHIFT]
TEST
EDX,EDX
JS SHORT DSHIFT ; 除数是2的幂次
ADD EAX,EDX ; 矫正取整误差
JC
SHORT DOVERFL ; 矫正溢出
MUL [RECIPROCAL_DIVISOR] ;
与除数的倒数相乘
MOV EAX,EDX
DSHIFT: SHR EAX,CL ; 调整位数
RET
DOVERFL:MOV
EAX,[RECIPROCAL_DIVISOR] ; 被除数 = 0FFFFFFFFH
SHR
EAX,CL ; 用位移的方法做除法
RET
DIVIDE_FIXED ENDP
对于0 <= x < 232, 0 < d <
232的范围,这段代码得出的结果与DIV指令一致。
注意:如果你能保证x<0FFFFFFFFH的话,JC
DOVERFL及其跳转的目标代码都是不需要的。
如果除数是2的幂次的可能性很小,那么就不值得为它优化了,你可以省去到DSHIFT的跳转,而在case
A下,令CORRECTION = 0做一次乘法。
如果除数经常改变,那么SET_DIVISOR过程还需要优化。 在PPlain和PMMX处理器上,你可以用26.15节给出的代码取代BSR指令。
*浮点除法(所有处理器)
最高精度的浮点除法需要38或39个时钟周期。
你可以通过在浮点控制字中指定低精度来节省时间(在PPlain和PMMX上,只有FDIV和FIDIV在低精度下更快;而在PPro,PII和PIII上,对于FSQRT也同样在低精度下更快。
没有其它指令可以通过降低精度来提高速度)。
*并行做除法(PPlain和PMMX)
在PPlain和PMMX上,可以把一个浮点除法和一个整形除法同时做来节省时间。
而在PPro,PII和PIII上不可能,因为整数除法和浮点除法用了同一条电路。
比如:A = A1 / A2;
B = B1 / B2
FILD
[B1]
FILD [B2]
MOV EAX,
[A1]
MOV EBX, [A2]
CDQ
FDIV
DIV EBX
FISTP [B]
MOV [A], EAX
(要保证将浮点控制字的舍入控制位设置成期望的舍入方法)
*用倒数指令更快地做除法(PIII)
在PIII上你可以用快速倒数指令RCPSS或RCPPS求得除数的倒数,然后将被除数与之相乘。然后这样做的精度只有12位。你可以用Intel应用手册AP-803中的牛顿-拉弗森方法把精度提高到32位:
x0 = RCPSS(d)
x1 = x0 * (2 - d *
x0) = 2*x0 - d * x0 * x0
x0是直接用倒数指令得到的除数d的倒数的逼近;x1则是更精确的逼近。你必须在把倒数与被除数相乘之前先用这个公式:
MOVAPS XMM1, [DIVISORS] ;
载入除数
RCPPS XMM0, XMM1 ; 求得倒数的逼近
MULPS XMM1, XMM0 ; 牛顿-Raphson公式
MULPS XMM1, XMM0
ADDPS
XMM0, XMM0
SUBPS XMM0, XMM1
MULPS XMM0, [DIVIDENDS] ; 结果在XMM0中
这样,就可以在18个时钟周期内做4个精度为23位的浮点除法。
在浮点寄存器中重复牛顿-拉弗森公式进一步增加精度也是可能的,但不是很有利。
如果想将此方法用于整型除法,那么必须检查舍入误差。
下面的代码在大约42个时钟周期里,用了对整型压缩字的截断操作完成了4个除法。 在0<=被除数 < 7FFFFH,0 < 除数 <=
7FFFFH的范围内能得到正确的结果:
MOVQ
MM1, [DIVISORS] ; 载入4个除数
MOVQ MM2, [DIVIDENDS] ;
载入4个被除数
PUNPCKHWD MM4, MM1 ; 把除数解压成双字
PSRAD MM4, 16
PUNPCKLWD
MM3, MM1
PSRAD MM3, 16
CVTPI2PS XMM1, MM4 ; 把高位的两个除数转化成浮点数 MOVLHPS XMM1,
XMM1
CVTPI2PS XMM1, MM3 ; 把低位的两个除数转化成浮点数
PUNPCKHWD MM4, MM2 ; 把被除数解压成双字
PSRAD MM4, 16
PUNPCKLWD
MM3, MM2
PSRAD MM3, 16
CVTPI2PS XMM2, MM4 ; 把高位的两个被除数转化成浮点数
MOVLHPS XMM2, XMM2
CVTPI2PS
XMM2, MM3 ; 把低位的两个被除数转化成浮点数
RCPPS XMM0, XMM1 ;
求得除数倒数的逼近
MULPS XMM1, XMM0 ; 用牛顿-Raphson方法改进精度
PCMPEQW MM4, MM4 ; 同时,将MM4的位(4个字长)全置1
PSRLW MM4, 15
MULPS XMM1,
XMM0
ADDPS XMM0, XMM0
SUBPS XMM0, XMM1 ; 精度是23位的除数的倒数
MULPS XMM0, XMM2 ; 与被除数相乘
CVTTPS2PI MM0, XMM0 ; 把低位的两个结果截断成整数
MOVHLPS XMM0, XMM0
CVTTPS2PI MM3, XMM0 ; 把高位的两个结果截断成整数
PACKSSDW MM0, MM3 ; 把4个结果压缩到MM0
MOVQ MM3, MM1 ; 把结果与除数相乘。。。
PMULLW MM3, MM0 ; 检查舍入误差
PADDSW MM0, MM4 ; 加1,为后面的减少作补偿
PADDSW MM3, MM1 ; 加上除数。 除数应该>被除数 PCMPGTW MM3, MM2 ;
看结果是否太小了
PADDSW MM0, MM3 ; 如果不是太小的话,各个结果减1
MOVQ [QUOTIENTS], MM0 ; 存储4个结果
代码检查是否结果太小,如果不是太小的话,则做适当矫正。不需要检查结果是否太大。
*避免除法(所有处理器)
显然,你要尽量少做除法。
浮点除法中,除数是常量或者除数是同一个值的重复除法自然可以通过与除数的倒数相乘来完成。 但也有很多其它情况下,你可以减少除法次数。
比如:if(A/B>C)... 在B>0时可以改成if(A>B*C)... ,在B<0时改成if(A<B*C)... 。
A/B+C/D 可以改成 (A*D + C*B) /
(B*D)
如果你用的是整数除法,那么必须意识到在你把公式变形后舍入误差可能会不同。
27.3 释放浮点寄存器(所有处理器)
当你退出子过程的时候,必须释放所有用过的浮点寄存器,除了那些用于存储结果的寄存器。
释放1个寄存器的最快方法是FSTP ST。
在PPlain和PMMX上,释放2个寄存器的最快方法是FCOMPP;在PPro,PII和PIII上用FCOMPP或两次FSTP
ST都可以,它们都能够很好地适合解码序列。
建议不要用FFREE。
27.4 在浮点和MMX指令之间的切换(PMMX,PII和PIII)
如果在MMX指令后面可能会有浮点指令的话,你必须在最后一条MMX指令后面加上EMMS指令。
在PMMX上,在浮点和MMX之间切换的代价很大。
在EMMS后面的第一条浮点指令大约要花58个额外的时钟周期,在浮点指令后面的第一条MMX指令大约要花38个额外的时钟周期。
在PII和PIII上没有这么大的代价。
可以在EMMS和第一条浮点指令之间插入一些整形指令来掩盖延迟。
27.5 把浮点数转化成整数(所有处理器)
浮点数转化为整数总是要通过存储单元才能完成,反之亦然:
FISTP DWORD PTR [TEMP]
MOV EAX, [TEMP]
在PPro,PII和PIII上,因为FIST指令相对较慢,故这段代码试图在向[TEMP]的写操作尚未完成时从[TEMP]读,可能会有惩罚(参考第17章)。 插入WAIT指令也无济于事(参考26.6节)。
如果可能的话,推荐你在[TEMP]的写操作与[TEMP]的读操作之间插入一些其它指令以避免惩罚。 这个方法对以下所有的例子都有效。
C/C++规范要求浮点数转化成整数用的是截断的方法,而不是舍入。
在转化时,大多数C库在FISTP指令之前先改变浮点控制字使其指示截断操作,过后再把它改回。 在任何处理器上这个方法都非常慢。
在PPro,PII和PIII上,浮点控制字是不能被重命名的,因此后面所有的浮点指令必须等待FLDCW指令引退,无法重叠执行。
每当在C/C++中你想进行浮点转为整数的操作时,应该考虑是否可以用舍入到最接近的整数来取代截断操作。
如果标准库中没有快速舍入函数,那么可以根据需要用下面的示例代码。
如果你在循环内部需要截断操作,则最好把改回控制字的操作放在循环外。
当然前提是循环内部的其余浮点指令可以在截断模式下正确工作。
就像下面的示例那样,你可以用种种技巧做截断操作而不改变控制字。
这些例子都假定控制字被设为默认值——也就是最近舍入(偶)。
最近舍入(偶)
; extern "C" int round (double
x);
_round PROC NEAR
PUBLIC
_round
FLD QWORD PTR [ESP+4]
FISTP DWORD PTR [ESP+4]
MOV
EAX, DWORD PTR [ESP+4]
RET
_round ENDP
截断操作(趋向0)
; extern "C" int truncate
(double x);
_truncate PROC NEAR
PUBLIC _truncate
FLD QWORD PTR
[ESP+4] ; x
SUB ESP, 12 ; 为局部变量分配空间
FIST DWORD PTR [ESP] ; 舍入值
FST DWORD PTR [ESP+4] ; 原来的浮点值
FISUB DWORD PTR [ESP] ; 减去舍入值
FSTP DWORD PTR [ESP+8] ; 存储减去舍入值后得到的差
POP EAX ; 舍入值
POP ECX ;
原来的浮点值
POP EDX ; 减去舍入值后得到的差 (浮点值)
TEST ECX, ECX ; 看x的符号
JS
SHORT NEGATIVE
ADD EDX, 7FFFFFFFH ; 如果x-round(x)
< -0,则会产生进位
SBB EAX, 0 ; 如果x-round(x) <
-0则减1
RET
NEGATIVE:
XOR ECX, ECX
TEST EDX,
EDX
SETG CL ; 如果x-round(x) > 0,置CL=1
ADD EAX, ECX ; 如果x-round(x) > 0 则加1
RET
_truncate ENDP
截断操作(趋向负无穷大)
; extern "C" int ifloor (double
x);
_ifloor PROC NEAR
PUBLIC
_ifloor
FLD QWORD PTR [ESP+4] ; x
SUB ESP, 8 ; 为局部变量分配空间
FIST
DWORD PTR [ESP] ; 舍入值
FISUB DWORD PTR [ESP] ;
减去舍入值
FSTP DWORD PTR [ESP+4] ; 存储减去舍入值后得到的差
POP EAX ; 舍入值
POP EDX ;
减去舍入值后得到的差 (浮点值)
ADD EDX, 7FFFFFFFH ; 如果x-round(x)
< -0,则会产生进位
SBB EAX, 0 ; 如果x-round(x) <
-0则减1
RET
_ifloor ENDP
在-2^31< x
<2^31-1范围内,这些过程能够正确工作。 注意,它们不检查溢出或NAN。
PIII有单精度浮点数的截断操作指令:CVTTSS2SI 和 CVTTPS2PI。
如果对单精度满意的话,这些指令是相当有用的。
但如果为了使用这些截断指令,你必须把高精度浮点转化成单精度浮点的话,那么因为数值在转化过程中可能会被向上舍入,你会遇到问题。
*有选择地使用FISTP指令(PPlain和PMMX)
一般把浮点数转化成整数是这样做的:
FISTP DWORD PTR [TEMP]
MOV EAX, [TEMP]
另一个供选择的方法是:
.DATA
ALIGN 8
TEMP DQ ?
MAGIC DD 59C00000H ; 2^51 + 2^52的魔数
.CODE
FADD [MAGIC]
FSTP QWORD PTR
[TEMP]
MOV EAX, DWORD PTR [TEMP]
加上2^51+2^52的魔数,使得任何在-2^31~+2^31范围内整数在被存为双精度浮点数的时候,低32位是对齐的。
结果与你使用FISTP指令进行除了截断(趋向0)外所有舍入操作的结果是相同的。 但如果控制字指定了截断操作或碰到溢出的情况,那么与使用FISTP指令的结果不同。
如果想兼容老式的80287处理器的话,你可能还需要一条WAIT指令,见26.6节。
这个方法并不比使用FISTP快,但在PPlain和PMMX上,它给出了更好的调度机会——因为在FADD和FSTP之间有3个时钟的浪费,它可以用其它指令来填补。使用魔数的相反数,将一个数乘以或除以2的幂次的操作与上面相同。
此外,在加一个常量时,你也可以通过把一个加该常量的魔数来完成,当然必须是双精度浮点数。
27.6 用整型指令做浮点操作(所有处理器)
一般地,整型指令比浮点指令快,因此用整型指令做一些简单的浮点操作通常是有利的。 最典型的就是数据传输。 比如:
FLD QWORD PTR [ESI] / FSTP QWORD PTR [EDI]
改为:
MOV EAX,[ESI] / MOV EBX,[ESI+4]
/ MOV [EDI],EAX / MOV [EDI+4],EBX
*测试是否一个浮点数为0:
一般浮点值0被表示成32或64位0,但有个缺陷:符号位可能被置位!-0也被看作是一个合法的浮点数,比如把0与一个负值相乘,处理器可能会产生一个最高位(符号位)是1的0。
因此想测试浮点数是否是0的话,你不应该测试符号位。 比如:
FLD DWORD PTR [EBX] / FTST
/ FNSTSW AX / AND AH,40H / JNZ IsZero
用整型指令改写时,需要用ADD
EAX,EAX避免符号位可能为1带来的影响:
MOV EAX,[EBX] / ADD EAX,EAX / JZ
IsZero
如果是双精度浮点数(QWORD),那么只要测试32-62位就可以了。
如果它们是0,只要是个规格化的浮点数,那么低4字节一定也是0。
*测试是否为负:
对一个浮点数,如果符号位是1且其它位至少有一位为1,那么它是负的。
比如:
MOV EAX,[NumberToTest] / CMP EAX,80000000H / JA
IsNegative
*巧妙操纵符号位:
通过简单地操纵符号位,可以改变一个浮点数的符号。 比如:
XOR BYTE PTR [a] + (TYPE a) - 1, 80H
类似地,可以通过AND操作把符号位复位,得到浮点数的绝对值。
*数值比较:
浮点数的存储格式是唯一的,这就使你可以用整型指令比较浮点数,除了符号位之外。
如果保证两个浮点数都是规格化且都为正数,那么可以简单地将它们像整数那样比较,比如:
FLD [a] /
FCOMP [b] / FNSTSW AX / AND AH,1 / JNZ ASmallerThanB
改为:
MOV EAX,[a] / MOV EBX,[b] / CMP
EAX,EBX / JB ASmallerThanB
该方法只有在两个浮点数的精度相同,且保证没有一个数的符号位是1的前提下才正确。
如果可能有负值,那么必须把负数转化成二进制补码的形式,做带符号数的比较:
MOV EAX, [a]
MOV EBX, [b]
MOV ECX,
EAX
MOV EDX, EBX
SAR
ECX, 31 ; 拷贝符号位
AND EAX, 7FFFFFFFH ; 使符号位复位(令它=0)
SAR EDX, 31
AND EBX,
7FFFFFFFH
XOR EAX, ECX ; 如果符号位是1,需要转为二进制补码?
XOR EBX, EDX
SUB EAX,
ECX
SUB EBX, EDX
CMP
EAX, EBX
JL ASmallerThanB ; 带符号整数的比较
对于所有规格化浮点数,包括-0,该方法都能正确工作。
27.7 用浮点指令做整型操作(PPlain和PMMX)
*整型乘法(PPlain和PMMX)
在PPlain和PMMX上,浮点乘法比整型乘法快,但把整数因子转为浮点以及把乘法的结果再转为整型的代价很大。
因此,只有在相比乘法次数而言,需要的数据转换次数很少的前提下,用浮点做乘法才有它的优越性(如果用了非规格化的浮点操作数,似乎可以省去一些转化次数,但处理非规格化的浮点数相当慢,因此不是个好办法!)。
在PMMX上,MMX乘法指令比整型乘法快,可以被流水化,吞吐量为每个时钟1条乘法指令。因此在PMMX上,如果你能忍受16位精度的话,用MMX指令做快速的乘法是个不错的办法。
而在PPro,PII和PIII上,整型乘法指令比浮点乘法快。
*整型除法(PPlain和PMMX)
浮点除法并不比整型除法快,但在浮点单元做除法的时候,你可以同时做另一个整型操作(包括整型除法,但不能是整型乘法)(看上文的例子)。
*二进制转为十进制(所有处理器)
用FBSTP指令能简单方便地把二进制转为十进制——虽然不一定是最快的方法。
27.8 数据块的拷贝(所有处理器)
有很多方法可以移动数据块。 最常用的方法是REP
MOVSD,但在一定条件下,其它方法更快。
在PPlain和PMMX上,如果目的地址不在cache中的话,用浮点寄存器一次移动8个字节更快:
TOP: FILD QWORD PTR [ESI]
FILD QWORD PTR [ESI+8]
FXCH
FISTP QWORD PTR
[EDI]
FISTP QWORD PTR [EDI+8]
ADD ESI, 16
ADD EDI, 16
DEC ECX
JNZ TOP
源数组和目的数组要按8对齐。虽然FILD和FISTP指令较慢,会花些额外的时间,但事实上你做的写操作次数只有原来的一半,因而得到了补偿。
注意,该方法只有在PPlain和PMMX上且目的地址不在1级cache的前提下才有优势。
你不能用FLD和FSTP(不带I)指令,因为位串是任意的,非规格化的“浮点数”处理的速度很慢,而且会造成某些位串在处理过程中被改变。
在PMMX上,如果目的地址不在cache中的话,用MMX指令一次移动8个字节更快。
TOP: MOVQ MM0,[ESI]
MOVQ [EDI],MM0
ADD ESI,8
ADD EDI,8
DEC ECX
JNZ TOP
考虑到可能会cache失效,故不需要把循环展开或做进一步优化,因为这种情况下主存的访问是瓶颈而不是指令的执行。
在PPro,PII和PIII上,如果满足以下条件,REP
MOVSD指令特别快(见26.3节):
*源地址和目的地址都按8对齐
*增量是向前的(清方向标志)
*计数器(ECX)大于等于64
*EDI和ESI的差在数值上大于等于32
*源内存和目的内存的必须是写回或组合写模式(一般你可以假定是这样)
在PII上,在上面的条件不满足且目的地址可能在1级cache的前提下,用MMX寄存器更快。
可以把循环进行2-展开,源地址和目的地址当然要按8对齐。
在PIII上,在上面的条件不满足或者目的地址在1级或2级cache的前提下,用MOVAPS移动数据最快:
SUB EDI, ESI
TOP: MOVAPS XMM0, [ESI]
MOVAPS
[ESI+EDI], XMM0
ADD ESI, 16
DEC ECX
JNZ TOP
不像FLD,MOVAPS可以处理任何位串。
要记住的是源地址和目的地址必须按16对齐。
如果移动的字节数不是16的倍数,那么你可以向上取最接近的能被16整除的数值,在目的缓存的末尾增加一些额外空间以接受多余字节。
如果这样做不可能,那么对于剩余的字节你只能想其它方法来移动了。
在PIII上,你还可以绕过cache,直接向RAM写,用的是MOVNTQ或MOVNTPS指令。
如果你不希望目的数据进入cache的话,这样做是很有效的。MOVNTPS只比MOVNTQ稍微快一点点。
27.9 自修改代码(所有处理器)
在对一块代码修改后立即执行带来的惩罚,在PPlain上大约是19个时钟,PMMX上大约31个时钟,PPro,PII和PIII上大约150-300时钟。
在80486等早期处理器上还需要在被修改代码和修改命令之间加一个jump,目的是为了刷新代码cache。
在受保护的操作系统下,为了得到修改代码的权限你需要进行特殊的系统调用:在16位Windows下,调用ChangeSelector;在32位Windows下,调用 VirtualProtect和FlushInstructionCache(或者把代码放到数据段内)。
不认为自修改代码是一个好的编程习惯,但如果因此在速度上的获利相当大的话也未尝不可。
27.10 检测处理器类型(所有处理器)
对一种处理器而言最优的代码不一定对另一种处理器最优,我想这点应该是很清楚了。
你可以把程序的重要部分写成几个不同的版本,每一个都对专门的处理器是最优的,然后检测程序究竟运行在哪个处理器上,再在运行时选择(也可能是运行前安装)相应版本的代码。
如果你用了不能被所有处理器支持的指令(也就是条件传输,FCOMI,MMX和XMM指令),那么你必须检测将要运行的处理器是否支持这些指令。下面的子过程检测了处理器的类型以及支持的特性。
; 如果汇编器不能识别CPUID指令,那么要事先定义它:
CPUID MACRO
DB 0FH, 0A2H
ENDM
; C++ 原型:
; extern "C" long int DetectProcessor (void);
; 返回值:
; bits 8-11 = 类型 (PPlain and PMMX是5, PPro, PII和PIII是6)
; bit 0 = 浮点指令支持
; bit 15 =
条件传输和FCOMI指令支持
; bit 23 = MMX指令支持
; bit 25 = XMM指令支持
_DetectProcessor PROC NEAR
PUBLIC _DetectProcessor
PUSH
EBX
PUSH ESI
PUSH EDI
PUSH EBP
;
检测处理器是否支持CPUID指令
PUSHFD
POP EAX
MOV EBX, EAX
XOR EAX, 1 SHL 21 ; 检测CPUID位是否可靠?
PUSH EAX
POPFD
PUSHFD
POP EAX
XOR EAX, EBX
AND EAX, 1 SHL
21
JZ SHORT DPEND ; 不支持CPUID指令
XOR EAX, EAX
CPUID ;
得到CPUID的功能数?
TEST EAX, EAX
JZ SHORT DPEND ; 不支持CPUID功能1?
MOV EAX, 1
CPUID ;
得到处理器类型和支持的特性
AND EAX, 000000F00H ; 类型
AND EDX, 0FFFFF0FFH ; 特征位
OR EAX, EDX ; 按位组合
DPEND: POP
EBP
POP EDI
POP ESI
POP EBX
RET
_DetectProcessor ENDP
注意,还有些操作系统禁止XMM指令。
关于检测操作系统是否支持XMM的资料在Intel应用手册AP-900找:“处理器和操作系统中,多媒体SIMD扩展支持标志("Identifying
support for Streaming SIMD Extensions in the Processor and Operating System")”。
更多关于微处理器标识的文章可在Intel的应用手册AP-485中找到:“英特尔处理器标识和CPUID指令("Intel Processor
Identification and the CPUID Instruction")”。
为了在不能识别条件传输,MMX,XMM等指令的汇编器上对这些指令进行编码,可以用 www.agner.org/assem/macros.zip 提供的宏。