3.2 常规的优化方案
本节概括了Intel体系结构的重要的常规优化技术。
3.2.1 寻址方式
在奔腾处理器上,当一个寄存器被用作基地址元素时,如果该寄存器是前一个指令的目的寄存器(假设所有的指令都已在预取队列中),将耗费一个附加的时钟周期,例如:
add esi, eax ;esi是目的寄存器
mov eax, [esi] ;esi是基地址,增耗费1个时钟
因为奔腾处理器有两条整数流水线,如果一个寄存器是前一时钟内任意指令的目的寄存器,这个用于计算有效地址(在任一管道)的基地址或索引元素的寄存器,将耗费一个额外的时钟周期.这种效应称为地址生成互锁(AGI)。为了避免AGI,指令间应安排其它指令,并产生至少一个时钟周期的间隔来分隔这些指令。
新增的MMX™寄存器不能当作索引寄存器或基地址使用,所以AGI不适用于MMX™寄存器为目的寄存器的情况。
在 AGI情况下,动态执行(P6-系列)处理器不产生额外迟延。
注意一些具有隐含寄存器读/写的指令,那些通过ESP(PUSH,POP,RET,CALL)而具有隐含的地址生成的指令,也会产生 AGI 附加迟延,如下例:
sub esp, 24
;一个时钟周期的阻塞
push ebx
mov esp, ebp
;一个时钟周期的阻塞
pop ebp
PUSH和POP也对ESP进行隐含的写操作,但是如果下一条指令是通过ESP寻址时,将不产生 AGI。奔腾处理器通过PUSH和POP指令中的ESP“重命名”来避免AGI的额外迟延,如下例:
push edi ;无阻塞
mov ebx, [esp]
在具有MMX™技术的奔腾处理器上,包含立即数和偏移量的指令可在U管道中进行配对。如果有必要使用常数,那么使用立即数通常比把常数读取到寄存器更有效。但是如果同一个立即数被多次引用,应把常数先取到一个寄存器,然后多次使用这个寄存器,这种方法将更快一些,如下例所示:
mov result, 555 ;555是一个立即数,result是偏移量
mov word ptr[esp+4], 1 ;1是立即数,4是偏移量
由于MMX™指令是双字节操作码(0x0F操作码映射),任何一个使用基地址或使用具有4字节位移的索引寻址来访问内存的指令,其指令长度为8字节,超过7字节的指令一次只能部分译码,应尽量避免使用(见3.4.2节)。人们经常通过将立即数值加到基地址或索引寄存器的方法来减少这种指令的长度,因此要除去立即数部分。
在Intel486™处理器中,当一个部分寄存器被写后,紧接着立即使用全部寄存器时,将产生一个附加的时钟周期。奔腾处理器在这方面无此后果,这种情况称之为部分阻塞条件,下例为奔腾处理器的例子。
mov al, 0 ;1
mov [ebp], eax ;2-在奔腾处理器上无迟延
下列为Intel486™处理器对应的例子。
mov al,0 ;1
;2-产生一个附加时钟周期
mov [ebp],eax ;3
动态执行(P6-系列)处理器具有与Intel486™处理器一样的阻塞类型,甚至耗费更高。在对部分寄存器写操作结束前,读操作一直被阻塞。这样的耗费可能多于一个时钟周期。
为达到最佳的性能,应避免在对部分寄存器(如AL,AH,AX)写操作后,使用包含这个部分寄存器的大寄存器。这条规则将防止动态执行处理器上的部分条件阻塞,并适用于所有大小寄存器对:
AL AH AX EAX
BL BH BX EBX
CL CH CX ECX
DL DH DX EDX
SP ESP
BP EBP
SI ESI
DI EDI
有关部分寄存器阻塞的其它内容,请见第3.2.4节。
3.2.2 对齐
本节提供了有关奔腾和动态执行(P6-系列)处理器在代码和数据对齐方面的信息。
3.2.2.1 代码
奔腾和动态执行(P6-系列)处理器有一个32字节的高速缓存线。由于预取缓冲区按16字节的边界提取,代码的对齐对预取缓冲区的效率有直接影响。
为使Intel体系结构系列处理器达到最佳性能,推荐如下方法:
循环的人口标号应按下一个0 MOD l6对齐,当它与边界的距离小于8字节时。
跟在条件转移后的标号不应被对齐。
在无条件转移或函数调用后的标号应按下一个0 MOD l6 对齐,当它与边界的距离小于8字节时。
3.2.2.2 数据
在奔腾处理器上, 对一个在高速缓存或总线上的未对齐数据进行访问,至少多耗费3个时钟周期。在动态执行(P6-系列)处理器上,对一个跨高速缓存线的末对齐数据进行访问,将耗费9-12个时钟周期。Intel推荐对数据按下述边界对齐,使全部处理器达以最佳执行性能。
2字节数据
一个2字节的对象应完全包含在按4字节对齐的字内。(即,它的二进制地址应为XXXX00,XXXX01,XXXXl0,而不能是XXXX11)。
4字节数据
4字节对象应按4字节边界对齐。
8字节数据
一个8字节数据(64位,如双精度实数据类型,全部MMX™成组寄存器值)应按8字节边界对齐。
3.2.3 有前缀的操作码
在奔腾处理器上,一个指令的前缀能够延缓语法分析并禁止指令配对。
下表强调了FIFO中的指令前缀的影响。
前缀为0F指令无附加迟延。
前缀为66h,67h指令将花费一个时钟检测前缀,一个时钟计算指令长度和一个时钟进入FIFO(总共3个时钟)。它必须是第一条进入FIFO的指令,且另一条指令可与它一起被压入FIFO。
具有其它前缀(非0F,66H,67H)的指令需要另一个附加时钟检测每一前缀。这些被压入FIFO的指令只能作为第一条指令。具有两个前缀的指令需用三个周期进入FIFO(两个时钟周期处理前缀,一个时钟周期处理指令)。另一个指令可以和第一条指令在同一时钟周期内一起压入FIFO。
仅当FIFO保持两个以下人口时,才存在对性能的影响。只要译码器(D1阶段)有两条指令来译码,就没有额外开销。如果以每个时钟周期两条指令的频率,将FIFO中的指令撤出,FIF0就可迅速变空。所以,如果恰好位于一条有前缀的指令前的一些指令造成了性能损失(如,由高速缓存失误造成的阻塞引起不能配对、未对齐等等),则造成有前缀的指令的性能损失可能被掩起来。
在动态执行(P6-系列)处理器中,长度上超过7字节的指令将降低每时钟周期内译码指令数(见第2.1.2节),前缀给指令增加了一到二个字节,可能使译码器受到限制。
建议在任何时候都尽可能地不使用有前缀的指令,或将它们安排在因其它原因造成阻塞的指令后面。
有关有前缀指令的配对详情,参见第3.3节。
3.2.4 动态执行(P6系列)处理器中的部分寄存器阻塞
在动态执行(P6-系列)处理器中,当16或8位寄存器(如AL,AH,AX)被写后立即执行一个32位寄存器(如EAX)读操作,那么读操作被阻塞直到写结束(最少7个时钟周期)。考虑下面的例子,第一条指令移动数值8到AX寄存器,接下来的指令访问大寄存器EAX,这个代码导致一个部分寄存器阻塞。
MOV AX, 8 ←─┐
ADD ECX, EAX ←─┘ 发生在访问 EAX寄存器的部分阻塞
对于全部8位和16位或32位寄存器时同样如此。
小寄存器 大寄存器
AL AH AX EAX
BL BH BX EBX
CL CH CX ECX
DL DH DX EDX
奔腾处理器不存在这种附加迟延。
由于P6系列的处理器可以按乱序执行代码。因此,这种相邻的指令不会产生阻塞。下例中也包含了一个部分阻塞。
MOV AL, 8 ←──┐
MOV EDX, 0x40 │
MOV EDI, new-value │
ADD EDX, EAX ←─┘发生在访问EAX寄存器的部分阻塞
另外,任何跟在被阻塞的微操作后的微操作,也将等待被阻塞的微操作通过管道后,才能获得执行的时钟周期。通常为了避免阻塞,在对16位或8位小寄存器(AL)写操作后,不要对包含它的大寄存器进行读操作。
为了使代码能够简便地应用于不同类型的处理器,在动态执行处理器中也存在读写小寄存器和大寄存器的特殊情况。下例的特殊情况中使用了XOR和SUB指令。
xor eax, eax
movb al, mem8
use eax ←无部分阻塞
xor eax, eax
movw al, meml6
use eax ←无部分阻塞
sub ax, ax
movb al, mem8
use eax ←无部分阻塞
sub eax, eax
movb al, mem8
use eax ←无部分阻塞
xor ah, ah
movb al, mem8
use eax ←无部分阻塞
通常,在实现这些指令序列时,总是在对寄存器写操作前,先对大寄存器清零。在这种特殊情况中,由XOR和SUB实现清零功能,且对EAX、EBX、ECX、EDX、EBP、ESP、EDI和ESI均有效。
3.2.5 有关分支预测的信息
对动态执行(P6-系列)的处理器来说,分支优化是最重要的优化方案。这些优化方案也同样有益于奔腾处理器。
3.2.5.1 动态分支预测
下列三个因素对动态分支预测是很重要的:
如果指令地址不在BTB中,预测结果为无分支的继续运行(失败)。
预测到的分支有一个时钟周期的迟延。
BTB存贮了有关分支预测历史的4位数据。
第一个因素建议将分支跟在将被执行的代码后面。决不要将数据跟在分支后面。
为避免因提取分支而产生一个时钟周期的迟延,可简单地在这些分支之间加入一些额外工作。这个迟延限定了循环的最小耗费为两个时钟周期。如果你的小循环不超过两个时钟周期,就将它展开。
分支预测器能够正确地对常规的分文模式进行预测。比如,它可以正确地预测出一个分支在循环中仅在每一奇次迭代时发生,而在每一偶次迭代中不发生。
3.2.5.2 在动态执行(P6系列)处理器上的静态预测
在动态执行处理器中,对那些在BTB中没有历史数据的分支将使用静态预测算法进行预测,静态预测算法如下:
预测无条件转移分支。
预测向后的条件分支,本规则对循环适用。
预测向前的条件分支。
静态预测的额外开销为6个时钟周期。无预测或错误预测的额外开销大于12个时钟周期。下面的图表显示了静态预测的算法。
图3-2 动态执行(P6-系列)的静态分支预测算法
下例说明了静态预测算法的基本规则。
A. Begin: MOV EAX, mem32
AND EAX, EBX
IMUL EAX, EDX
SHLD EAX, 7
JC Begin
在这个例子中,在第一次通过时,向后分支不在BTB中,因此BTB未进行预测,但是静态预测器将预测到这个分支,所以不产生预测失误。
B. MOV EAX, mem32
AND EAX, EBX
IMUL EAX, EDX
SHLD EAX, 7
JC Begin
Begin: Call Convert
本代码段的第一个分支指令(JC Begin)是一个向前条件分支。第一次通过时,它不在BTB中,但静态预测器将预测到该分支。
第一次通过时BTB发现Call Convert指令,但在BTB中没有进行预测。但静态预测算法能预测并提取一个该调用,这对一个无条件分支来说是正确的结果。
这些例子中,条件分支只有两种情况:提取的和未提取的。间接分支,如开关语句,可计算的GOTO或通过指针的调用,都可跳到一个特定的地址单元上。如果分支有一个偏离的目标地址(即分支90%都转向同一地址),那么BTB将在大多数情况能够精确预测到。但是如果目标地址是不可预测的,性能将迅速下降,用可预测的条件分支替换间接分支可以改善性能。