Intel, AMD及VIA CPU的微架构(16)

5.12.      选择最优的指令

使用更高效的指令替换较低效的指令有许多种可能性。最重要的前行汇总如下。

INC与DEC

这些指令有部分标记访问的问题,如第54页所示。总是以ADDEAX, 1等替换INCEAX。

8位与16位整数

用MOVZXEAX, BYTE PTR [MEM8]替换MOV AL, BYTE [MEM8]

用MOVZXEBX, WORD PTR [MEM16]替换MOV BX, WORD PTR [MEM16]

避免使用寄存器AH,BH,CH,DH的高8位。

如果8位或16位寄存器可以被并行封装及处理,那么使用MMX或XMM寄存器。

这些规则也适用于16位模式。

内存写

大多数内存写指令使用2个μop。类型为MOV[MEM], EAX的简单写指令仅使用一个μop,如果内存操作数没有SIB字节。如果有多个指针寄存器,或者有一个比例索引寄存器,或者如果ESP被用作基址寄存器,SIB字节是需要的。短形式的写指令可以使用通用寄存器(参考第41页)。例如:

;Example 5.15. uop counts for memory stores

movarray[ecx], eax          ; 1 uop

movarray[ecx*4], eax      ; 2 uops becauseof scaled index

mov[ecx+edi], eax            ; 2 uopsbecause of two index registers

mov[ebp+8], ebx               ; 1 uop

mov[esp+8], ebx                ; 2 uopsbecause esp used

moves:[mem8], cl             ; 1 uop

moves:[mem8], ch            ; 2 uops becausehigh 8-bit register used

movq[esi], mm1                ; 2 uops because not a generalpurp.register

fstp[mem32]                      ; 2 uopsbecause not a general purp.register

所有对应的内存读指令都仅使用1个μop。这些规则的一个推论是,对栈上局部变量有许多次写入的例程应该使用EBP作为指针,而有许多次读及少量写的例程应该使用ESP作为指针,并保存EBP用于其他目的。

偏移与旋转(rotate)

在P4上,整形寄存器上偏移与旋转相对慢,因为整数执行单元将数据传输到MMX偏移单元,接着传回来。向左偏移可由加法替换。例如,SHLEAX, 3可由三次ADDEAX, EAX替换。这不适用于P4E,它的偏移比加法快。

应该避免1以外值或者CL的带进位旋转(RCL,RCR)。

如果代码包含许多整数偏移及乘法,在P4上在MMX或XMM寄存器里执行它可能更有利。

整数乘法

在P4上整数乘法是慢的,因为整数执行单元将数据传输到FP-MUL单元,再传回来。如果代码有许多整数乘法,那么在MMX或XMM寄存器里处理数据可能更有利。

一个常量的整数乘法可以被加法替代。当然,由一长串ADD指令替换一条乘法指令仅应该在关键依赖链中完成。

LEA

在P4与P4E上LEA指令被分解为加法与偏移。带有一个比例因子的LEA指令最好用加法替换。这仅适用于LEA指令,不适用于带有包含一个比例因子的内存操作数的其他指令。

在64位模式中,带有一个RIP相对地址的LEA是缺乏效率的。用MOVRAX, OFFSET MEM替换LEA RAX, [MEM]。

使用FP,MMX及XMM寄存器的寄存器到寄存器移动

下面的指令从一个寄存器拷贝到另一个,在P4上时延都是6个时钟周期,在P4E上是7个:MOVQMM, MM,MOVDQAXMM, XMM,MOVAPSXMM, XMM,MOVAPDXMM, XMM,FLDST(X),FSTST(X),FSTPST(X)。这些指令没有额外的时延。这些指令长时延的一个可能原因是,它们使用与内存写相同的执行单元(端口0,MOV)。

有几个方式避免这个时延:

·        有时可以通过把相同的寄存器重复用作其他指令的源,而不是目标,消除拷贝一个寄存器的需求。

·        使用浮点寄存器,通常可以通过使用FXCH,消除从一个寄存器移动数据到另一个寄存器的需求。FXCH指令没有时延。

·        如果需要拷贝一个寄存器的值,在最关键依赖路径使用旧拷贝,在不那么关键路径中使用新拷贝。下面的例子计算Y= (a + b)2.5

;Example 5.16. Optimize register-to-register moves

fld[a]

fadd[b]       ; a+b

fldst            ; Copy a+b

fxch             ; Get old copy

fsqrt            ; (a+b)0.5

fxch             ; Get new (delayed) copy

fmulst, st   ; (a+b)2

fmul            ; (a+b)2.5

fstp[y]

旧拷贝用于慢的平方根,而6-7个时钟周期后可用的新拷贝用于乘法。

如果这些方法都无济于事,且时延比吞吐率更重要,那么使用更快的替代者:

·        对80位浮点寄存器:

fldst(0) ; copy register

可由以下替换

fldz                             ; make anempty register

xoreax, eax               ; set zero flag

fcmovzst, st(1)         ; conditional move

·        64位MMX寄存器:

movqmm1, mm0

可由混排指令替换

pshufwmm1, mm0, 11100100B

·        128位寄存器

movdqaxmm1, xmm0

可由混排指令替换

pshufdxmm1, xmm0, 11100100B

 或者更快的:

pxorxmm1, xmm1 ; Set new register to 0

por  xmm1, xmm0 ; OR with desired value

这些方法的时延都比寄存器到寄存器移动要小。不过,这些技巧的一个缺点是,它们使用同样用于所有这些寄存器上计算的端口1。如果端口1饱和了,使用去往端口0、慢的移动可能更好。

5.13.      P4与P4E中的瓶颈

在优化一段代码时,找出控制执行速度的限制因素很重要。调整不合适的因子很可能没有任何好处。在下面段落中,我将解释每个可能的限制因素。你必须依次考虑每个因素来确定哪个是最窄的瓶颈,然后集中优化这个元素,直到它不再是最窄的瓶颈。

内存访问

如果程序正在访问大量数据,或者如果数据分散在内存各处,那么我们见有许多数据缓存不命中。访问未缓存数据是如此耗时,其他所有优化考虑都不再重要。缓存被组织为每64字节一个对齐行。如果访问一个对齐的64字节块中的一个字节,那么我们可以确定,所有的64字节将被载入1级数据缓存,可以无需额外代价访问。为了改进缓存,建议在程序相同部分使用的数据保存在一起。你可以对齐大的数组与数据结构到64字节边界。如果没有足够的寄存器,将局部变量保存在栈上。

在P4上,1级数据缓存仅有8kb,P4E上是16kb。要保存所有的数据,这可能不够,但在P4/P4E上,2级数据缓存比之前的处理器高效。从2级缓存获取数据仅需额外几个时钟周期。

不太可能被缓存的数据,可以在使用之前预取。如果内存地址被连续访问,那么它们将被自动预取。因此你应该最好以线性的方式组织数据,使它们能被连续访问,在程序的关键部分,不要访问超过4个大数组,越少越好。

在你访问未缓存数据且不能依赖自动预取的情形下,PREFETCH指令可以改进性能。不过,在P4上过度使用PREFETCH指令会降低程序吞吐率。如果你对REPFETCH指令对程序是否有好处心存疑虑,取而代之,你可以只是把需要的数据载入空闲寄存器。如果没有空闲寄存器,那么使用不改变任何寄存器的读内存操作数指令,比如CMP或TEST,因为栈指针不太可能成为任何关键依赖链的部分,一个预取数据的有用方法是CMPESP, [MEM],它将仅改变标记。

在写入一个不太可能很快访问的内存位置时,你可以使用非临时写指令MOVNTI等,不过在P4上过度使用非临时移动会降低性能。

关于内存访问更多的指引,可以参考《IntelPentium 4 and Intel Xeon Processor Optimization Reference Manual》

执行时延

一个依赖链的执行时间可以从手册4“指令表”中列出的时延来计算。当后续指令去往另一个执行单元时,许多指令有额外的1时钟周期的时延。进一步解释,参考第50页。

如果长的依赖链限制了程序的性能,你可以通过选择低时延指令、尽量减少执行单元间迁移、打破依赖链,以及利用一切机会并行地计算子表达式来提高性能。

在依赖链中总是要避免内存中介,如第54页所示。

执行单元吞吐率

如果依赖链是短的,或者如果正在并行地在几个依赖链上工作,那么程序很可能受到吞吐率而不是时延的限制。不同的执行单元有不同的吞吐率。处理简单整型指令及其他常见μop的ALU0与ALU1都有每时钟周期2条指令的吞吐率。大多数其他执行单元有每时钟周期1条指令的吞吐率。在使用128位寄存器时,吞吐率通常是每两个时钟周期1条指令。除法与平方根的吞吐率最低。每个吞吐率测量应用于所有执行在同一个执行子单元的μop(参考第46页)。

如果执行吞吐率限制了代码,那么尝试将一些计算移到其他执行子单元。

端口吞吐率

每个执行端口每时钟周期可以接受一个μop。端口0与端口1每半个时钟周期可以接受一个额外的μop,如果这些μop去往双倍速单元ALU0与ALU1。如果代码关键部分的所有μop都去往端口1下的单倍速单元,那么吞吐率将被限制为每时钟周期1个μop。如果在4个端口最优地分配μop,那么吞吐率可以高达每时钟周期6个μop。这样一个高吞吐率仅能在短突发中实现,因为追踪缓存及回收站限制平均吞吐率为名时钟周期少于3个μop。

如果端口吞吐率限制了代码,那么尝试将一些μop移到其他端口。例如,可用MOVREGISTER, MEMORY替代MOV REGISTER, IMMEDIATE。

追踪缓存交付

追踪缓存每时钟周期可交付大约3个μop。在P4上,某些μop要求多个追踪缓存项,如第40页所示。对包含许多分支以及对包含分支的小循环,每时钟周期的交付率小于3μop(参考第42页)。

如果上述因素没有限制程序的性能,那么你应该以每时钟周期大约3个μop为目标。

选择生成μop数量最少的指令。在P4上避免要求多个追踪缓存项的μop(参考第40页)。

追踪缓存大小

使用相同的物理芯片面积,追踪缓存能保存的代码少于传统的代码缓存。如果程序的关键部分不能放入追踪缓存,追踪缓存有限的面积会是一个严重的瓶颈。

μop回收

回收站每时钟周期可以处理3个μop。被采用分支仅能由回收站三个工位中的第一个处理。

如果你以每时钟周期大约3个μop为目标,那么避免过量的跳转、调用及分支。小的关键循环μop的个数最好是3的倍数(参考第52页)。

指令解码

如果代码的关键部分不能放入追踪缓存,那么受限步骤可能是指令解码。解码器每时钟周期可以处理一条指令,只要该指令生成不超过4个μop,没有微代码,而且没有太多前缀(参考第43页)。如果解码是瓶颈,你可以尝试尽量减少指令数,而不是μop数。

分支预测

时延与吞吐率的计算仅在所有的分支都被预测中时才有效。当吞吐率是限制因素时,分支误预测会严重影响性能。P4不能在误预测后取消无效的μop会严重降低性能。

在代码关键部分避免可预测性差的分支,除非替代者(比如条件移动)由于增加了额外的依赖性与时延,得不偿失。细节参考第15页。

μop重演

在缓存不命中、失败的写到读转发等之后,P4通常在重演无效μop上浪费太多资源。这会导致严重的性能下降,特别是在长依赖链中存在内存中介时。

相关文章
相关标签/搜索