Pentium 及 MMX 代码的优化

常规的优化方案

[第一节规则列表及建议][第三节:调度]


  本节概括了 Intel 体系结构的重要的常规优化技术.

2.1 寻址方式

  在奔腾处理器上, 当一个寄存器被用作基地址元素时, 如果该寄存器是前一个指令的目的寄存器(假设所有的指令都已在预取队列中), 将耗费一个附加的时钟周期. 例如:


       add esi, eax      ;esi是目的寄存器
       mov eax, [esi]    ;esi是基地址,增耗费1个时钟
  因为奔腾处理器有两条整数流水线, 如果一个寄存器是前一时钟内任意指令的目的寄存器, 这个用于计算有效地址(在任一管道)的基地址或索引元素的寄存器, 将耗费一个额外的时钟周期.这种效应称为地址生成互锁(AGI). 为了避免 AGI, 指令间应安排其它指令, 并产生至少一个时钟周期的间隔来分隔这些指令.

  新增的 MMXTM寄存器不能当作索引寄存器或基地址使用, 所以 AGI不适用于 MMXTM寄存器为目的寄存器的情况.

  在 AGI情况下, 动态执行(P6-系列)处理器不产生额外迟延.

  注意一些具有隐含寄存器读/写的指令, 那些通过 ESP (PUSH, POP, RET, CALL)而具有隐含的地址生成的指令, 也会产生 AGI 附加迟延, 如下例:


       sub   esp, 24  
	                  ;一个时钟周期的阻塞
       push  ebx
       mov   esp, ebp 
                      ;一个时钟周期的阻塞
       pop   ebp

  PUSH 和 POP也对ESP进行隐含的写操作, 但是如果下一条指令是通过 E3P寻址时, 将不产生 AGI. 奔腾处理器通过 PUSH 和 POP指令中的 ESP "重命名" 来避免入AGI的额外迟延, 如下例:


       push  edi       ;无阻塞
       mov   ebx,[esp]

  在具有 MMXTM技术的奔腾处理器上, 包含立即数和偏移量的指令可在 U管道中进行配对. 如果有必要使用常数, 那么使用立即数通常比把常数读取到寄存器更有效. 但是如果同一个立即数被多次引用, 应把常数先取到一个寄存器, 然后多次使用这个寄存器, 这种方法将更快一些, 如下例所示:


       mov  result, 555        ;555是一个立即数, result是偏移量
       mov  word ptr[esp+4],1  ;1 是立即数, 4是偏移量

  由于MMXTM指令是双字节操作码(0x0F操作码映射), 任何一个使用基地址或使用具有4字节位移的索引寻址来访问内存的指令, 其指令长度为8字节. 超过7字节的指令一次只能部分译码, 应尽量避免使用(见4.2节). 人们经常通过将立即数值加到基地址或索引寄存器的方法来减少这种指令的长度, 因此要除去立即数部分.

  在 Intel486TM处理器中, 当一个部分寄存器被写后, 紧接着立即使用全部寄存器时, 将产生一个附加的时钟周期. 奔腾处理器在这方面无此后果, 这种情况称之为部分阻塞条件, 下例为奔腾处理器的例子.


       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

  有关部分寄存器阻塞的其它内容, 请见第2.4节

2.2 对齐

  本节提供了有关奔腾和动态执行(P6-系列)处理器在代码和数据对齐方面的信息.

2.2.1 代码

  奔腾和动态执行(P6-系列)处理器有一个32字节的高速缓存线. 由于预取缓冲区按16字节的边界提取, 代码的对齐对预取缓冲区的效率有直接影响.

  为使 Intel体系结构系列处理器达到最佳性能, 推荐如下方法:

2.2.2 数据

  在奔腾处理器上, 对一个在高速缓存或总线上的未对齐数据进行访问, 至少多耗费3个时钟周期. 在动态执行(P6-系列)处理器上, 对一个跨高速缓存线的末对齐数据进行访问, 将耗费9-12个时钟周期. Intel推荐对数据按下述边界对齐, 使全部处理器达以最佳执行性能.

2字节数据

  一个2字节的对象应完全包含在按4字节对齐的字内. (即,它的二进制地址应为 XXXX00, XXXX01, XXXXl0, 而不能是XXXX11).

4字节数据

  4字节对象应按4字节边界对齐.

8字节数据

  一个8字节数据(64位, 如双精度实数据类型, 全部 MMXTM成组寄存器值) 应按8字节边界对齐.

2.3 有前缀的操作码

  在奔腾处理器上, 一个指令的前缀能够延缓语法分析并禁止指令配对.

  下表强调了 FIFO 中的指令前缀的影响.

  仅当 FIFO保持两个以下人口时, 才存在对性能的影响. 只要译码器(D1阶段)有两条指令来译码, 就没有额外开销. 如果以每个时钟周期两条指令的频率, 将 FIFO中的指令撤出, FIF0就可迅速变空. 所以, 如果恰好位于一条有前缀的指令前的一些指令造成了性能损失 (如, 由高速缓存失误造成的阻塞引起不能配对、未对齐等等), 则造成有前缀的指令的性能损失可能被掩起来.

  在动态执行(P6-系列)处理器中, 长度上超过7字节的指令将降低每时钟周期内译码指令数 (见1.2节), 前缀给指令增加了一到二个字节, 可能使译码器受到限制.

  建议在任何时候都尽可能地不使用有前缀的指令, 或将它们安排在因其它原因造成阻塞的指令后面.

  有关有前缀指令的配对详情, 参见第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位寄存器时同样如此.

小寄存器
ALAHAX
BLBHBX
CLCHCX
DLDHDX
大寄存器
EAX
EBX
ECX
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均有效.

2.5 有关分支预测的信息

  对动态执行(P6-系列)的处理器来说, 分支优化是最重要的优化方案. 这些优化方案也同样有益于奔腾处理器.

2.5.1 动态分支预测

  下列三个因素对动态分支预测是很重要的:

  1. 如果指令地址不在 BTB中, 预测结果为无分支的继续运行(失败).
  2. 预测到的分支有一个时钟周期的迟延.
  3. BTB存贮了有关分支预测历史的4位数据.

  第一个因素建议将分支跟在将被执行的代码后面. 决不要将数据跟在分支后面.

  为避免因提取分支而产生一个时钟周期的迟延, 可简单地在这些分支之间加入一些额外工作. 这个迟延限定了循环的最小耗费为两个时钟周期. 如果你的小循环不超过两个时钟周期, 就将它展开.

  分支预测器能够正确地对常规的分文模式进行预测. 比如, 它可以正确地预测出一个分支在循环中仅在每一奇次迭代时发生, 而在每一偶次迭代中不发生.

2.5.2 在动态执行(P6-系列)处理器上的静态预测

  在动态执行处理器中, 对那些在 BTB中没有历史数据的分支 将使用静态预测算法进行预测. 静态预测算法如下:

  静态预测的额外开销为6个时钟周期. 无预测或错误预测的额外开销大于12个时钟周期. 下面的图表显示了静态预测的算法.

  向前条件分支未发生 (fallthrough)
    if <条件> {
	 ...   ↓
     }                         无条件分支 
    for <条件> {           JMP 
	 ...   ↓                    ───→ 
	 }
    JMP
  向后条件分支发生
    loop {
 ↑
 └─} <条件>

  下例说明了静态预测算法的基本规则.


    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 将在大多数情况能够精确预测到. 但是如果目标地址是不可预测的, 性能将迅速下降, 用可预测的条件分支替换间接分支可以改善性能.


云风工作室 制作