优化汇编例程(16)

16. 有问题的指令

16.1. LEA指令(所有处理器)

对许多应用,LEA指令是有用的,因为它可以在一条指令里执行一个偏移操作,两个加法,及一个移动。例子:

; Example 16.1a, LEA instruction

lea eax, [ebx+8*ecx-1000]

; Example 16.1b

mov eax, ecx

shl eax, 3

add eax, ebx

sub eax, 1000

快得多。

LEA的一个典型用法是3寄存器加法:lea eax, [ebx+ecx]。LEA可用于执行一个加法或偏移,而不改变标记。

处理器有没有文档记录的只带有比例索引寄存器的取址模式。因此,像lea eax, [ebx*2]的指令实际上编码为带有4字节立即数位移的lea eax, [ebx*2+00000000H]。可以写成lea  eax, [ebx+ebx],减少这条指令的大小。如果你恰好有一个寄存器是零(像循环后的循环计数器),可以使用它作为一个基址寄存器来减小代码大小:

; Example 16.2, LEA instruction without base pointer

lea eax, [ebx*4]                                                                                        ; 7 bytes

lea eax, [ecx+ebx*4]                                                                                ; 3 bytes

基址与索引寄存器的大小可以通过地址大小前缀来改变。目标寄存器的大小可使用操作数大小前缀来改变(参考第20页,前缀)。如果操作数的大小比地址大小小,结果被截断。如果操作数大小超过地址大小,结果被零扩展。

64模式中,LEA最短的版本有32位操作数大小及64位地址大小,即LEA EAX, [RBX+RCX],参考第72页。在结果确定小于232时,使用这个版本。在地址可能大于232时,使用带有64位目标寄存器的版本。

在某些处理器上,LEA比加法慢。在某些处理器上,带有比例因子及偏移的LEA更复杂的形式比简单形式更慢。每个处理器的细节参考手册4《指令表》。

32位模式中优先选择的版本有32位操作数与32位地址。在AMD处理器上带有16位操作数的LEA是慢的。在32位模式中,应该避免带有16位地址的LEA,因为在许多处理器上解码地址大小前缀是慢的。

LEA也可用在64位模式中载入一个RIP相对地址。RIP相对地址不能与基址或索引寄存器组合。

16.2. INC与DEC

INC与DEC指令不会修改进位标记,但会修改其他算术标记。在P4与P4E上,仅写标记寄存器一部分的代价是一个额外μop。在某些Intel处理器上,如果后续指令读进位标记或所有标记位,它会导致一个部分标记暂停。在所有的处理器上,它会导致对之前指令进位标记的一个假依赖。

在对速度优化时,使用ADD与SUB。在对大小优化或预期没有性能损失时,使用INC与DEC。

16.3. XCHG(所有处理器)

指令XCHG register, [memory]是危险的。这条指令总是有一个隐含的LOCK前缀,强制与其他处理器或核同步。因此,这条指令非常耗时,总是应该避免,除非锁是有意为之的。

在优化大小时,带有寄存器操作数的XCHG指令可能是有用的,如第68页解释的那样。

16.4. ​​​​​​​偏移(shift)与旋转(rotate)(P4)

在P4上通用寄存器上的偏移与旋转是慢的。你可以考虑使用MMX或XMM寄存器,或者使用加法替代左移。     

16.5. ​​​​​​​通过进位旋转(所有处理器)

在所有处理器上,使用CL或1以外的计数RCR与RCL是慢的,应该避免。

16.6. ​​​​​​​比特测试(所有处理器)

在较旧的处理器上,最好通过像TEST、AND、OR、XOR或偏移这样的指令替代指令BT、BTC、BTR与BTS 。在Intel处理器上,应该避免带有内存操作数的比特测试。在AMD处理器上,BTC、BTR与BTS使用2个μop。在优化大小时,比特测试指令是有用的。

16.7. ​​​​​​​LAHF与SAHF(所有处理器)

在P4与P4E上,LAHF是慢的。使用SETcc来保存一个标记的值。

在P4E与AMD处理器上,SAHF是慢的。使用TEST测试AH中的一个比特。如果可用,使用FCOMI作为序列FCOM / FNSTSW AX/ SAHF的替代。

在某些早期的64位Intel处理器上,LAHF与SAHF在64位模式中不可用。

16.8. ​​​​​​​整数乘法(所有处理器)

依赖于处理器,整数乘法需要3到14时钟周期。因此,以其他指令的一个组合,比如SHL、ADD、SUB与LEA,代替一个常量的乘法通常是有利的。例如IMUL EAX, 5可以被LEA EAX, [EAX + 4 * EAX]替换。

16.9. ​​​​​​​除法(所有处理器)

在所有处理器上,整数除法与浮点除法都相当耗时。减少除法数量的各种方法在手册1《优化C++软件》中解释。改进包含除法代码的几个方法在下面讨论。

2指数的整数除法(所有处理器)

2指数的整数除法可由右移完成。一个无符号整数除2N:

; Example 16.3. Divide unsigned integer by 2^N

shr eax, N

一个有符号整数除2N:

; Example 16.4. Divide signed integer by 2^N

cdq

and edx, (1 shl N) - 1                                                                              ; (Or: shr edx,32-N)

add eax, edx

sar eax, N

显然,如果被除数确定不是负数,应该优先选择无符号版本。

整数除以常量(所有处理器)

浮点值可以通过乘一个常量的倒数来除这个常量。如果我们希望使用同一个寄存器,我们必须放大倒数2n倍,然后将积右移n。有各种算法找出n的一个合适值并补偿取整误差。下面描述的算法由挪威的Terje Mathisen发明,没有在别处发布过。在Henry S. Warren的书《Hacker's Delight》,Addison-Wesley 2003,以及论文:T. Granlund与P. L. Montgomery:《Division by Invariant Integers Using Multiplication, Proceedings of the SIGPLAN 1994 Conference on Programming Language Design and Implementation》,可以找到其他方法。

下面的算法将给出使用截断的无符号整数除法的准确结果,即与DIV指令的结果相同:

b = (d中有意义(significant)的比特数) - 1

r = w + b

f = 2r / d

如果f是一个整数,那么d是2的指数:去往case A。

如果f不是一个整数,那么检查f的小数部分是否 < 0.5

如果f的小数部分 < 0.5:去往case B。

如果f的小数部分 > 0.5:去往case C。

case A (d = 2b):

result = x SHR b

case B (f的小数部分 < 0.5):

将f向下取整到最接近的整数

result = ((x+1) * f) SHR r

case C (f的小数部分 > 0.5):

将f向上取整到最接近的整数

result = (x * f) SHR r

例子:

假定你希望除5。

5 = 101B。

w = 32。

b = (有意义的二进制数个数) - 1 = 2

r = 32+2 = 34

f = 234 / 5 = 3435973836.8 = 0CCCCCCCC.CCC...(16进制)

小数部分超出一半:使用case C。

将f向上取整到0CCCCCCCDH。

下面的代码将EAX除5,在EDX在返回结果:

; Example 16.5a. Divide unsigned integer eax by 5

mov edx, 0CCCCCCCDH

mul edx

shr edx, 2

在乘法后,EDX包含右移32位的积。因为r = 34,你必须再右移2位来得到结果。要除10,只需将最后一行改为SHR EDX, 3。

在case B中,我们将有:

; Example 16.5b. Divide unsigned integer eax, case B

add eax, 1

mov edx, f

mul edx

shr edx, b

这个代码对x的所有值都有效,除了0FFFFFFFFFH给出0,因为ADD EAX, 1指令的溢出。如果x = 0FFFFFFFFFFH是可能的,将代码改为:

; Example 16.5c. Divide unsigned integer, case B, check for overflow

mov edx, f

add eax, 1

jc DOVERFL

mul edx

DOVERFL: shr edx, b

如果x的值受限,那么可以使用较小的r,即更少数字。使用较小的r有几个原因:

  • 可以设置r = w = 32,避免最后的SHR EDX, b。
  • 可以设置r = 16+b,并使用产生32位而不是64位结果的乘法指令。这将解放EDX寄存器:

; Example 16.5d. Divide unsigned integer by 5, limited range

imul eax,0CCCDH

shr eax,18

  • 可以选择支持case C,而不是case B的r的值,避免ADD EAX, 1指令

在这些情形里,x的最大值是至少2r-b-1,有时更大。如果你希望知道要代码正确工作,x的实际最大值,你必须进行系统的测试。

如果乘法慢,你可能希望以偏移与加法指令替换乘法指令,如第147页所述。

下面的例子将EAX除10,并在EAX中返回结果。我选择了r=17,而不是19,因为它恰好给出了更容易优化的代码,并对x覆盖了相同的区域。f = 217 / 10 = 3333H,case B:q = (x+1)*3333H:

; Example 16.5e. Divide unsigned integer by 10, limited range

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正确工作。

该除法方法也可用于向量操作数。例如16.5f将8个无符号16位整数除10:

; Example 16.5f. Divide vector of unsigned integers by 10

.data

align 16

RECIPDIV DW 8 dup (0CCCDH) ; Vector of reciprocal divisor

.code

pmulhuw xmm0, RECIPDIV

psrlw xmm0, 3

反复使用相同除数的整数除法(所有处理器)

如果在汇编时刻除数未知,但反复使用这个除数除,那么使用什么的方法是有利的。这个除法仅进行一次,而对每个被除数都进行乘法与偏移操作。该算法的一个没有分支的变种,在上面引用的Granlund与Montgomery的论文里提供。

这个方法实现在asmlib函数库中,用于有符号与无符号整数,以及向量寄存器。

浮点除法(所有处理器)

使用手册1《优化C++软件》中描述的方法,两个或更多浮点除法可以被合并为两个。

浮点除法所需的时间依赖于精度。在使用x87形式的浮点寄存器时,可以在浮点控制字中声明一个较低的精度使除法变快。这还加速了FSQRT指令,但其他指令不会。在使用XMM寄存器时,不必改变任何控制字。如果应用程序允许,仅使用单精度指令。

不可能同时执行浮点除法与整数除法,因为在大多数处理器上,它们使用相同的执行单元。

使用倒数指令进行快速除法(具有SSE的处理器)

在具有SSE指令集的处理器上,可以在除数上使用快速倒数指令RCPSS或RCPPS,然后乘以被除数。不过,精度仅有12比特。你可以通过使用在Intel的应用程序备注AP-803里提到的Newton-Raphson方法,将精度提高到23位:

x0 = rcpss(d);

x1 = x0 * (2 - d * x0) = 2 * x0 - d * x0 * x0;

其中x0是除数d倒数的第一个近似,x1是一个更好的近似。在乘以被除数之前,你必须使用这个公式。

; Example 16.6, fast division, single precision (SSE)

movaps xmm1, [divisors]                                                   ; load divisors

rcpps xmm0, xmm1                                                             ; approximate reciprocal

mulps xmm1, xmm0                                                            ; newton-raphson formula

mulps xmm1, xmm0

addps xmm0, xmm0

subps xmm0, xmm1

mulps xmm0, [dividends]                                                    ; results in xmm0

在Core2上,在大约18时钟周期里,这进行了4次精度为23位的除法。通过重复双精度的Newton-Raphson公式,进一步提升精度是可能的,但不是很有优势。

​​​​​​​16.10. 字符串指令(所有处理器)

没有重复前缀的字符串指令太慢,应该被更简单的指令替代。这同样适用于所有处理器上的LOOP,及某些处理器上的JECXZ。如果重复计数不是太小,REP MOVSD与REP STOSD相当快。总是尽可能使用最大的字大小(32位模式中DWORD,64位模式中QWORD),确保源与目标都对齐到字大小。不过,在许多情形里,使用XMM寄存器会更快。在大多数情形里,在XMM寄存器中移动数据比REP MOVSD及REP STOSD更快,特别在较旧的处理器上。细节参考第165页。

注意,REP MOVS指令向目标写一个字,在同一时钟周期,它从源读下一个字。在P2与P3上,如果这两个地址的比特2 ~ 4相同,你会有一个缓存库冲突。换而言之,如果ESI+WORDSIZE-EDI被32整除,每迭代你将有1时钟周期的额外性能损失。避免缓存库冲突最容易的方法是将源与目标对齐到8。在优化代码中不要使用MOVSB或MOVSW,即使在16位模式里。

在许多处理器上,通过一次移动16字节或一整个缓存行,REP MOVS与REP STOS可以执行得很快。这仅在满足某些条件时发生。依赖于处理器,典型地,快速字符串指令的条件有:计数必须大,源与目标必须对齐,必须是前向的,源与目标的距离必须至少是缓存行大小,源与目标的内存类型必须是回写或写合并(通常你可以假定满足后一个条件)。

在这些条件下,速度与你使用向量寄存器移动一样快,在某些处理器上甚至更快。

尽管字符串指令可以相当便利,必须强调,在许多情形里其他解决方案更快。如果上面的快速移动条件不满足,使用其他方法有很多好处。REP MOVS的替代,参考第165页。

REP LOADS,REP SCAS与REP CMPS每迭代比简单循环需要更多时间。REPNE SCASB的替代,参考第139页。

16.11. ​​​​​​​向量化字符串指令(具有SSE4.2的处理器)

SSE4.2指令集包含4条非常高效的、用于字符串查找及比较操作的指令:PCMPESTRI,PCMPISTRI,PCMPESTRM,PCMPISTRM。有两个向量寄存器,每个包含一个字符串与确定在这些字符串上执行什么操作的立即数。这4条指令可以进行下面每个类型的操作,区别仅在输入与输出操作数:

  • 在一个集合里查找字符:找出在第二个向量操作数中哪些字节属于由第一个向量操作数中字节定义的集合,在一次操作中比较所有256个可能的组合。
  • 在一个范围里查找字符:找出在第二个向量操作数中哪些字节在由第一个向量操作数调用的范围内。
  • 字符串比较:确定两个字符串是否相同。
  • 子字符串查找:找出由第一个向量操作数定义的一个子字符串,在第二个向量操作数中的所有出现。

这两个字符串的长度可以显式指定(PCMPESTRx),或使用一个结尾零隐含指定(PCMPISTRx)。在当前Intel处理器上,后者更快,因为它有更少的输入操作数。任意结尾零及超出结尾的字节被忽略。输入可以是一个比特掩码或字节掩码(PCMPxSTRM)或一个索引(PCMPxSTRI)。其他输出可以在以下标记中给出。进位标记:结果非零。符号标记:第一个字符串结尾。零标记:第二个字符串结尾。溢出标记:结果第一个比特。

对各种字符串解析与处理任务,这些指令非常高效,因为它们在一次操作中进行大及复杂的任务。不过,对简单任务,比如strlen及strcopy。当前的实现比最好的替代方案慢。

可以在www.agner.org/optimize/asmlib.zip处我的函数库里找到例子。

16.12. ​​​​​​​WAIT指令(所有处理器)

通常,你可以通过忽略WAIT指令(也称为FWAIT)来提升速度。WAIT指令有3个功能:

  1. 旧式8087处理器在每条浮点指令前要求一条WAIT,来确保协处理器准备好接收它。
  2. WAIT用于协调浮点单元与整数单元间的内存访问。例子:

; Example 16.7. Uses of WAIT:

B1: fistp [mem32]

      wait                                                                                     ; wait for FPU to write before..

      mov eax,[mem32]                                                            ; reading the result with the integer unit

B2: fild [mem32]

       wait                                                                                     ; wait for FPU to read value..

       mov [mem32], eax                                                           ; before overwriting it with integer unit

B3: fld qword ptr [ESP]

        wait                                                                                    ; prevent an accidental interrupt from..

        add esp, 8                                                                          ; overwriting value on stack

  1. 有时WAIT用于检查异常。如果之前的浮点指令设置了浮点状态字中的一个非掩蔽异常比特,它将产生一个中断。

关于A

A点的功能除了旧式8087处理器,其他处理器不再需要。除非你希望你的16位代码与8087兼容,你应该通过声明一个更高级的处理器,告诉汇编器不要放入这些WAIT。一个8087浮点模拟器还会传染WAIT指令。因此,你应该告诉汇编器不要产生模拟代码,除非你需要。

关于B

协调内存访问的WAIT指令在8087与80287上绝对需要,但在Pentium上不需要。在80387与80486上它是否需要,不是很清楚。我在这些Intel处理器上做过几个测试,在任何32位Intel处理器上,忽略WAIT都不会引发任何错误,尽管Intel手册说,对这个目的,WAIT是需要的,除了在FNSTSW与FNSTCW后面。忽略用于协调内存访问的WAIT指令不是100%安全,即使是32位代码,因为代码可能运行在一个非常罕见的、要求WAIT的80386主处理器与287协处理器组合上。同样,我没有关于非Intel处理器的信息,我也没有测试所有可能的硬件及软件组合,因此可能有需要WAIT的其他情形。

如果你希望确保你的代码在最旧的32位处理器上也是有效的,我建议你包含这里的WAIT,以策安全,如果罕见且过时的硬件平台可以排除,比如80386与80287的组合,那么你可以忽略WAIT。

关于C

出于这个目的,在以下指令前,汇编器自动插入WAIT:FCLEX,FINIT,FSAVE,FSTCW,FSETNV,FSTSW。你可以通过写FNCLEX等来忽略WAIT。我的测试显示,在大多数情形里,WAIT是不必要的,因为没有WAIT,这些指令仍然将在异常上产生一个中断,除了80387上的FNCLEX与FNINIT。(在从中断返回的IRET是指向FN..指令,还是指向下一条指令方面,有一些不一致)。

几乎所有的其他浮点指令也将产生一个中断,如果之前的浮点指令设置了一个非屏蔽异常比特,因此迟早会检测到异常。你可以在程序最后一条浮点指令后面插入一条WAIT,确保捕捉所有的异常。

如果希望确切知道在何处发生一个异常,以从这个情形恢复,你可能仍然需要WAIT。例如,考虑上面B3下的代码:如果你希望能从一个FLD产生的异常中恢复,那么你需要WAIT,因为在ADD ESP, 8之后的异常可能改写要读的值。在某些处理器上FNOP可能比WAIT快且能做相同用途。

16.13. ​​​​​​​FCOM + FSTSW AX(所有处理器)

在所有的处理器上。FNSTSW指令非常慢。大多数处理器有FCOMI指令可避免慢的FNSTSW。使用FCOMI,而不是常见序列FCOM / FNSTSW AX / SAHF将节省4 ~ 8时钟周期。因此,你应该尽可能使用FCOMI来避免FNSTSW,即使是在它耗费额外的代码的情形里。

在没有FCOMI指令的P1与PMMX处理器上,执行浮点比较的通常方式是:

; Example 16.8a.

fld [a]

fcomp [b]

fstsw ax

sahf

jb ASmallerThanB

你可以通过使用FNSTSW AX,而不是FSTSW AX,直接测试AH,而不是不可成对的SAHF,来改进这个代码:

; Example 16.8b.

fld [a]

fcomp [b]

fnstsw ax

shr ah,1

jc ASmallerThanB

测试零或相等性:

; Example 16.8c.

ftst

fnstsw ax

and ah, 40H                                                           ; Don't use TEST instruction, it's not pairable

jnz IsZero                                                                ; (the zero flag is inverted!)

测试是否更大:

; Example 16.8d.

fld [a]

fcomp [b]

fnstsw ax

and ah, 41H

jz AGreaterThanB

在P1与PMMX上,FNSTSW指令需要2时钟周期,但在任何浮点指令后面,它被推迟额外4时钟周期,因为它要等待从流水线回收状态字。你可以通过整数指令来填补这个空档。

有时使用整数指令比较浮点值会更快,如第161与163页所述。

16.14. ​​​​​​​FPREM(所有处理器)

在所有处理器上,FPERM与FPEM1是慢的。你可以通过下面的算法来替代它:乘以除数倒数,减去截断值获得小数部分,然后乘以除数。(如何在没有截断指令的处理器上截断值,参考第160页)。

有些文档称这些指令可能会给出不完整的约简,因此重复FPERM或FPERM1指令,直到约简完成是必须的。我在几个处理器上做了测试,从旧式的8087开始,我没有发现需要重复FPERM或PFERM1的情形。

16.15. ​​​​​​​FRNDINT(所有处理器)

在使用处理器上,这条指令是慢的。替代它,使用:

; Example 16.9.

fistp qword ptr [TEMP]

fild qword ptr [TEMP]

这段代码更快,尽管在写完成前,尝试从[TEMP]读可能有性能损失。建议在两者间放入其他指令来避免这个性能损失。在没有截断指令的处理器上如何截断,参考第160页。在具有SSE指令的处理器上,使用转换指令,比如CVTSS2SI与CVTTSS2SI。

16.16. ​​​​​​​FSCALE与指数函数(所有处理器)

在所有处理器上,FSCALE是慢的。可以通过在浮点数的指数域插入期望的指数,快得多地计算2指数整数。要计算2N,其中N是有符号整数,从下面的例子中选择合适你N范围的那个:

对|N| < 27-1,可以使用单精度:

; Example 16.10a.

mov eax, [N]

shl eax, 23

add eax, 3f800000h

mov dword ptr [TEMP], eax

fld dword ptr [TEMP]

对|N| < 210-1,可以使用双精度:

; Example 16.10b.

mov eax, [N]

shl eax, 20

add eax, 3ff00000h

mov dword ptr [TEMP], 0

mov dword ptr [TEMP+4], eax

fld qword ptr [TEMP]

对|N| < 214-1,使用长双精度:

; Example 16.10c.

mov eax, [N]

add eax, 00003fffh

mov dword ptr [TEMP], 0

mov dword ptr [TEMP+4], 80000000h

mov dword ptr [TEMP+8], eax

fld tbyte ptr [TEMP]

在具有SSE2指令的处理器上,可以在XMM寄存器中进行这些操作,无需内存中间数(参考第163页)。

FSCALE通常用在指数函数的计算里。下面的代码展示了一个没有慢的FRNDINT与FSCALE指令的指数函数:

; Example 16.11. Exponential function

; extern "C" long double exp (double x);

_exp PROC NEAR

PUBLIC _exp

      fldl2e

      fld qword ptr [esp+4]                                                                                    ; x

      fmul                                                                                                                  ; z = x*log2(e)

      fist dword ptr [esp+4]                                                                                   ; round(z)

      sub esp, 12

      mov dword ptr [esp], 0

      mov dword ptr [esp+4], 80000000h

      fisub dword ptr [esp+16]                                                                              ; z - round(z)

      mov eax, [esp+16]

      add eax, 3fffh

      mov [esp+8], eax

      jle short UNDERFLOW

      cmp eax, 8000h

      jge short OVERFLOW

      f2xm1

      fld1

      fadd                                                                                                                 ; 2^(z-round(z))

      fld tbyte ptr [esp]                                                                                          ; 2^(round(z))

      add esp, 12

      fmul                                                                                                                  ; 2^z = e^x

      ret

UNDERFLOW:

      fstp st

      fldz                                                                                                                    ; return 0

      add esp, 12

      ret

OVERFLOW:

      push 07f800000h                                                                                           ; +infinity

      fstp st

      fld dword ptr [esp]                                                                                         ; return infinity

      add esp, 16

      ret

_exp ENDP

16.17. ​​​​​​​FPTAN(所有处理器)

根据手册,FPTAN返回两个值,X与Y,让程序员把Y除X来得到结果;但事实上,它总是在X中返回1,因此你可以省略这个除法。我的测试显示在所有带有浮点单元或协处理器的32位Intel处理器上,不管什么算法,FPTAN总是在X中返回1。如果你希望绝对确保代码在所有处理器上正确运行,那么可以测试X是否是1,这比除X快。Y值可能非常大,但不会是无穷,因此你不需要测试Y是否包含有效值,如果你知道参数是合法的。

16.18. ​​​​​​​FSQRT(SSE处理器)

在具有SSE的处理器上计算平方根近似值的一个快速方法是,x乘以x的平方根倒数:

sqrt(x) = x * rsqrt(x)

指令RSQRTSS或RSQRTPS给出精度为12位的平方根倒数。可以通过使用在Intel的应用注解AP-803中描述的Newton-Raphson公式,把精度提高到23位:

x0 = rsqrtss(a)

x1 = 0.5 * x0 * (3 - (a * x0) * x0)

其中x0是a平方根倒数的第一个近似,x1是一个更好的近似。求值的次序是重要的,你必须在乘以a得到平方根之前,使用这个公式。

​​​​​​​16.19. FLDCW(大多数Intel处理器)

如果FLDCW指令后跟任何读扩展字的浮点指令(几乎所有的浮点指令都会这样做),许多处理器会有严重的暂停。

在编译C或C++代码时,通常产生大量的FLDCW指令,因为从浮点值转换到整数通过截断完成,而其他浮点指令使用取整。在翻译到汇编后,尽可能通过取整而不是截断,或者将FLDCW移出需要截断的循环,可以改进这个代码。

在P4上,这个暂停甚至更长,大约143时钟周期。但P4对这个情形做了特殊处理,控制字在两个不同值之间切换。这是C++程序中的典型情形,在一个浮点值转换为整数时,改变控制字来声明截断,在这个转换后变回取整。在读入的新值与之前FLDCW前面的控制字的值相同时,FLDCW的时延是3。不过,在向控制字读入与已有值相同的值时,如果这个值与较早时间的值不相同,时延仍是143。

如何无需改变控制字,将浮点值转换到整数,参考第160页。在具有SSE的处理器上,使用截断指令,比如CVTTSS2SI。

16.20. ​​​​​​​MASKMOV指令

掩蔽内存写指令MASKMOVQ与MASKMOVDQ极慢,因为它们绕过了缓存,写主内存(所谓的非临时写)。

VEX编码的替代VPMASKMOVD,VPMASKMOVQ,VMASKMOVPS,VMASKMOVPD都写到缓存,因此在Intel处理器上要快得多。在AMD Bulldozer与Piledriver处理器上,VEX版本仍然极慢。

应该不惜一切代价避免这些指令。替代方法参考章节13.8。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
混合编程是指在一个程序中同时使用汇编语言和C语言编写代码。这样做的好处是可以充分发挥汇编语言的高效性能,同时又可以利用C语言的易读性和可移植性。 下面是一个简单的例程,展示了如何在C语言中调用汇编语言函数: ```c #include <stdio.h> extern int asm_add(int a, int b); int main() { int a = 10, b = 20, sum; sum = asm_add(a, b); printf("sum = %d\n", sum); return 0; } ``` 上面的代码中,`asm_add`是一个汇编语言函数,在C语言中通过`extern`关键字声明。在`main`函数中,我们调用了`asm_add`函数,并将结果打印出来。 下面是`asm_add`函数的汇编语言实现: ```asm section .text global asm_add asm_add: mov eax, edi add eax, esi ret ``` 上面的代码中,`asm_add`函数接收两个参数,分别存放在寄存器`edi`和`esi`中。函数实现将这两个参数相加,并将结果返回。 对于上面的例程,我们需要将C语言和汇编语言代码分别保存为`.c`和`.asm`文件,并使用汇编器和编译器编译链接: ```bash nasm -f elf64 -o asm_add.o asm_add.asm gcc -c main.c gcc -o main main.o asm_add.o ``` 上述命令中,`-f elf64`参数指定了汇编语言程序的目标平台为x86-64,`-c`参数表示只编译不链接,最后一个命令将编译好的目标文件链接成可执行文件。 以上就是一个简单的混合编程例程,通过在C语言中调用汇编语言函数,实现了高效的计算。实际应用中,混合编程可以用于底层的系统编程和优化性能要求较高的算法实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值