以下优化建议,在不同微架构下收益差别较大:
优化CPU前端包括两方面:
分支优化对性能有显著地影响。通过理解分支流和提高其预测性,你可以显著提高代码性能。
帮助分支预测的方法有:
消除分支能够提高性能是因为:
有四个最重要的方法来消除分支:
Assembly/Compiler Coding Rule 1. (MH impact, M generality):
安排代码让基本代码块连续存放,并消除不需要的分支。
Assembly/Compiler Coding Rule 2. (M impact, ML generality) :
当分支跳转很难预测时,可以用CMOV和SETCC替代分支。值得注意的是,这两个指令相比成功的分支预测指令,会有额外的开销,因为它们其实在内部把两个分支都执行了。在使用这两个指令替换分支跳转指令时,要对比其总开销。
Memory Order Violation: load 和store指令是可以乱序执行,当load投机执行,先于store指令时,读取了错误的数据,就会导致这个性能 惩罚。
从Pentium 4开始的微架构,在自旋锁中加入PAUSE指令,可以显著地减少内存发生Memory Order Violation的概率。
静态分支预测算法,不占用BTB。
Assembly/Compiler Coding Rule 3. (M impact, H generality):
向前跳转分支时,安排最可能执行的代码放到条件分支 fall-through处。
向后跳转分支时,安排最不可能执行的代码放到条件分支 fall-through处。
有条件向前跳转为不跳转
//Forward condition branches not taken (fall through)
IF {….
↓ 预测执行这里
}
有条件向后跳转为跳转
//Backward conditional branches are taken
LOOP {…
预测继续执行循环体
↑ −− }
无条件跳转直接跳转
//Unconditional branches taken
JMP
——→ 预测直接执行跳转指令
return address stack:我估计是一个独立的硬件,它含有16个entries。用来记录返回地址。
call和return指令会打断流水线,因为他们造成了执行流程的跳转。用BTB可以提前预测call跳转,用return address stack则可以提前预测return跳转。当函数嵌套超过16个后,性能会下降。
请确保call指令和return指令严格配对,这样return address stack超过阈值的可能性会大大减少。
Assembly/Compiler Coding Rule 4. (MH impact, MH generality):
最近调用的call要和最近的return匹配。不要自己把返回地址压入用户栈然后call函数,这样会造成call和return不匹配。
call和return是昂贵的操作,使用inline替代它们:
Assembly/Compiler Coding Rule 5. (MH impact, MH generality):
如果inline一个函数可以减少代码大小,或者这个函数本身就很小,调用频率很高,那就inline它。
Assembly/Compiler Coding Rule 6. (H impact, H generality):
如果inline一个函数增加了代码大小,使它不能完全放在trace cache(这个应该只有Intel NetBust微架构才有)中,那么不要inline它。
Assembly/Compiler Coding Rule 7. (ML impact, ML generality):
函数嵌套超过了16个,考虑inline其中一些函数。
Assembly/Compiler Coding Rule 8. (ML impact, ML generality):
函数内部逻辑的分支预测成功率很低时,可以考虑inline这个函数。这是因为分支预测失败,导致提前执行的return指令无意义,会增加性能惩罚。
Assembly/Compiler Coding Rule 9. (L impact, L generality):
如果一个函数的最后阶段是调用另外一个函数,可以考虑优化成用jump替代call指令,这样节省了一个call/return对。这个工作编译器来做。
Assembly/Compiler Coding Rule 10. (M impact, L generality):
在一个16字节的指令块中不要超过4个分支。
Assembly/Compiler Coding Rule 11. (M impact, L generality):
在一个16字节的指令块中不要有超过2个循环逻辑。
仔细的安排代码布局可以增加cache和内存局部性。经常执行的基本代码块应该在内存中连续布局。不怎么执行的代码,比如错误处理,可以单独存放。
direct branches:jump的地址是具体数值。
indirect branch:jump的地址保存在寄存器或内存中,需要通过计算才能知道跳转地址。
Assembly/Compiler Coding Rule 12. (M impact, H generality):
如果uop是从DSB(Decode ICache)中获取的。最常执行的direct branches,应该把指令部分放在64B cache line的尽可能尾部,跳转指令的目的放在64B cache line的尽可能首部。
如果uop是从legacy decode pipeline中获取的。最常执行的direct branches,应该把指令部分放在16B对齐的内存块的尽可能尾部,跳转指令的目的放在16B对齐的尽可能首部。
没看明白,编译器怎么知道uop能从哪里来的,还能动态调整指令布局???懵逼???,或许即时编译器可以利用这个优化,看手册该优化效果还挺大的。求大神指点。
Assembly/Compiler Coding Rule 13. (M impact, H generality):
如果一个条件语句的执行体,不怎么执行,它应该放到程序的其他部分,如果这个执行体几乎不怎么执行,那它应该放到一个不同的code page。
继续懵逼,编译器究竟怎么利用这个点的???
indirect call:call的地址保存在寄存器或内存中,需要通过计算才能知道跳转地址,这将打断流水线。
对于indirect branch和indirect call指令,缺省是预测其会失败,流水线会使用其后一条指令(fall-through path, 没办法,跳转地址要计算才知道,缺省没法预测)。之后可以通过动态预测硬件来覆盖这个缺省选择。
对于indirect call,fall-through不会造成性能问题,即使预测错误,反正call的函数会return回来继续执行下一条指令,预测只是提前执行了而已。
Assembly/Compiler Coding Rule 14. (M impact, L generality):
把indirect branch最有可能执行的逻辑体紧跟在indirect branch后面。如果indirect branch经常执行并且不能被预测硬件预测,那就在indirect branch后跟一个UD2指令,阻止处理器来做投机执行。
indirect branch会使执行流跳转到任意位置。如果大部分时间都是到同一个位置,那么BTB会工作的很好。因为只有一个跳转地址会存在BTB中,太过随机的跳转会降低预测成功率。
indirect branch(比如switch语句)可以用多个条件分支来等价替换,这会增加BTB中的预测条目。添加条件分支来替代indirect branch的部分或全部跳转目标需要满足如下条件才会有更大收益:
User/Source Coding Rule 1. (M impact, L generality) :
如果一个indirect branch(比如switch语句)有2个或多个常用的跳转目标,至少有一个与分支历史相关联,那么可以把这个跳转目标用条件分支替代。
Loop Unrolling:循环展开,是一种牺牲程序的尺寸来加快程序的执行速度的优化方法。可以由程序员完成,也可由编译器自动优化完成。
循环展开最常用来降低循环开销,为具有多个功能单元的处理器提供指令级并行。也有利于指令流水线的调度。
induction variable:循环里那个递增递减变量。
common subexpression elimination:一种编译优化技术,它会搜索有没有等价表达式,是否值得用保存了计算结果的变量来替换它。
使用unrolling的好处:
使用unrolling的潜在坏处:
Assembly/Compiler Coding Rule 15. (H impact, M generality):
unroll小的循环,直到分支和induction variable开销小于执行开销的10%。
Assembly/Compiler Coding Rule 16. (H impact, M generality) :
避免unroll过多循环结构,这个可能会撑爆trace cache 或者 指令cache。
Assembly/Compiler Coding Rule 17. (M impact, M generality) :
对那种经常执行,而且迭代次数确定的循环,可以通过unroll把迭代次数降低到16次或更少。除非unroll会增加代码大小撑爆trace cache或指令cache。如果循环体理由多于一个的条件分支,可以unroll这个循环使得循环次数为16/(# conditional branches)。
消除了分支语句来提高性能。
一条指令它的操作数是一个寄存器和一个内存地址,另一条是该指令的操作数全是寄存器的版本,那么前者译码后的uop会比后者译码后的uop要多一些。但是如果如果要用寄存器-寄存器版本替换寄存器-内存版本,则需要2条连续指令,这很可能会降低取指令的吞吐量。
Assembly/Compiler Coding Rule 18. (ML impact, M generality):
为了提高取指令/译码吞吐量,如果内存版本的指令能从micro-fusion受益,那么优先使用这个版本,而不是全用寄存器的版本。
Macro-fusion可以把两条指令合并成一条uop。这两条指令必须满足条件。
第一条指令必须是CMP或者TEST,它可以是REG-REG, REG-IMM, 或者是 micro-fused REG-MEM comparison。第二条指令是一个条件分支。
4个译码器都能够译码一个macro-fused指令,这样译码峰值可以达到5条指令每时钟周期。
Assembly/Compiler Coding Rule 19. (M impact, ML generality):
尽可能的创造macro-fusion前提条件。TEST指令优先于CMP指令。尽可能使用unsigned variables 和 unsigned jumps。变量比较时要尝试验证变量为非负。尽可能避免MEM-IMM版本的TEST和CMP指令。
Assembly/Compiler Coding Rule 20. (M impact, ML generality):
如果能确保变量在做比较运算时,变量是非负值,那么软件可以启用macro-fusion; 如果把变量和0比较,可以用TEST指令来启动macro-fusion。
指令的长度最多可以到15字节。一些指令前缀可以动态改变指令长度,译码器必须能识别它。当predecoder遇到LCP时,必须使用一个较慢的译码算法,使predecoder译码从1个时钟周期上升到6个时钟周期。
能够动态改变指令长度的指令前缀包括:
Assembly/Compiler Coding Rule 21. (MH impact, MH generality):
产生代码时优先使用imm8或者imm32 代替 imm16。
如果一定要使用imm16,可以把高位填充0,装载到寄存器中,在使用该寄存器。
Double LCP Stalls
会产生LCP stall的指令,如果跨越了16字节取指令的边界,那么会触发两次LCP stall惩罚。如下的对齐情况会导致LCP stall惩罚两次。
为了避免LCP stalls两次惩罚,不要使用编码了SIB字节或者 addressing mode with byte displacement的LCP stall指令。
False LCP Stalls
False LCP stalls的特征跟LCP stalls相同,但是它发生在不带有imm16值得指令中。
发生条件为指令同时满足:
满足如下条件的循环,能够被LSD检测到,之后可以不再取指令,而是直接使用instruction queue中的uop,之后重复执行这个循环时,就不需要取指令,译码,而是直接使用队列中的uop。
LSD检测条件:
Assembly/Compiler Coding Rule 23. (MH impact, MH generality) Break up a loop long sequence of instructions into loops of shorter instruction blocks of no more than the size of LSD.
Assembly/Compiler Coding Rule 24. (MH impact, M generality) Avoid unrolling loops containing LCP stalls, if the unrolled block exceeds the size of LSD.
decoded ICache是从Sandy Bridge开始加入的新特性。它有两个优势:
确保hot代码能保存到decoded ICache:
Dense Read-Modify-Write Code
每个32字节对齐的内存块(代码段)最多只有18个uop能保存进Decoded ICache。因此,代码很密集时,可能会超过18个uop限制而不能加入Decoded ICache。 Read- modify-write (RMW) 指令就是这么个例子。
如下是一些可能的方案来使hot代码能够保存进Decoded ICache:
Align Unconditional Branches for Decoded ICache
每个32字节代码块里,只有最多3个无条件分支能保存到Decoded ICache。
无条件分支一般是jump tables 和 switch declarations。为了能让他们保存到Decoded ICache,解决方案是增加多个NOP指令。
Two Branches in a Decoded ICache Way
decoded ICache 是 8-ways 32-sets 结构。每way最多2个分支语句。在32字节对齐代码块中分支语句比较多时,它会阻止其他代码加入decoded ICache。解决方案还是加入NOP指令,把它弄得稀疏一点。
Assembly/Compiler Coding Rule 25. (M impact, M generality)
Avoid putting explicit references to ESP in a sequence of stack operations (POP, PUSH, CALL, RET).