Intel X86 优化指南阅读笔记--通用优化(前端)

PROCESSOR PERSPECTIVES

以下优化建议,在不同微架构下收益差别较大:

  • 指令译码的吞吐量很重要。利用好decoded ICache,Loop Stream Detector和macro-fusion能进一步提高CPU前端性能。
  • 充分利用好4个译码器来产生代码。利用好micro-fusion和macro-fusion,这样其中3个简单译码器就再被限制为只能译码只含有一条uop的简单指令。
  • Sandy Bridge, Ivy Bridge 和 Haswell微架构,优化代码大小来提高前端性能,取决于代码大小与decode ICache大小的关系。
  • 只使用一个寄存器的一部分来写入数据,在不同微架构下会导致差异很大的性能惩罚。为了避免部分写入寄存器引起的伪依赖,使用完整的寄存器写入。
  • 硬件预取通常能够有效减少内存延时和指令访问。但是不同微架构下可能需要自己选择特定的硬件预取实现算法。

OPTIMIZING THE FRONT END

优化CPU前端包括两方面:

  • 保持稳定的速度向cpu后端提供uop。错误的分支预测会打断uop流,或者导致后端在错误的uop路径上浪费资源。大量的调优工作聚焦在这个方面,利用Branch Prediction Unit。
  • 提供uop流来尽可能的充分利用执行带宽和retirement带宽。

Branch Prediction Optimization

分支优化对性能有显著地影响。通过理解分支流和提高其预测性,你可以显著提高代码性能。
帮助分支预测的方法有:

  • 保持代码和数据在不同page,这一点非常重要。
  • 尽可能的减少分支。
  • 安排代码使其符合静态分支预测算法。
  • 在spin-wait循环中使用PAUSE指令。
  • 使用inline函数,和让call和return指令尽可能配对。
  • 对于小于等于16次的循环,使用unroll编译指令把代码展开成顺序执行(除非这个展开使代码大小增加过大)。
  • Avoid putting two conditional branch instructions in a loop so that both have the same branch target address and, at the same time, belong to (i.e. have their last bytes’ addresses within) the same 16- byte aligned code block. (我还没弄明白)
Eliminating Branches

消除分支能够提高性能是因为:

  • 减少了分支错误预测的概率。
  • 减少了对branch target buffer (BTB) 的消耗。

有四个最重要的方法来消除分支:

  • 安排代码块连续执行。
  • 对循环利用unroll编译指令。
  • 使用CMOV指令。该指令是条件移动指令,伪代码:
    if (cond) {
    mov dst, src
    } else {
    nop
    }
    我们常用的分支跳转指令jump,当预测失败时,必须回退重新执行。但是CMOV不是分支跳转,他会顺序执行下去,保证了流水线的通畅。但是相应的,它的执行开销比jump指令大。
  • 使用SETCC指令。伪代码:
    IF condition
    THEN DEST ← 1;
    ELSE DEST ← 0;
    FI;
    同样的,该指令避免了分支跳转。

Assembly/Compiler Coding Rule 1. (MH impact, M generality):
安排代码让基本代码块连续存放,并消除不需要的分支。

Assembly/Compiler Coding Rule 2. (M impact, ML generality) :
当分支跳转很难预测时,可以用CMOV和SETCC替代分支。值得注意的是,这两个指令相比成功的分支预测指令,会有额外的开销,因为它们其实在内部把两个分支都执行了。在使用这两个指令替换分支跳转指令时,要对比其总开销。

Spin-Wait and Idle Loops ####

Memory Order Violation: load 和store指令是可以乱序执行,当load投机执行,先于store指令时,读取了错误的数据,就会导致这个性能 惩罚。

从Pentium 4开始的微架构,在自旋锁中加入PAUSE指令,可以显著地减少内存发生Memory Order Violation的概率。

Static Prediction ####

静态分支预测算法,不占用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
    ——→ 预测直接执行跳转指令

Inlining, Calls and Returns

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替代它们:

  • 可以省略参数传递开销。
  • 对编译器来说,inline函数有更大的几率可以优化。
  • 如果inline函数有分支跳转,包含inline函数的caller有更多的上下文来提高分支预测。
  • 分支预测失败在函数中的惩罚比在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个循环逻辑。

Code Alignment

仔细的安排代码布局可以增加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。
继续懵逼,编译器究竟怎么利用这个点的???

Branch Type Selection

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的部分或全部跳转目标需要满足如下条件才会有更大收益:

  • 条件分支必须与分支历史关联。之后的例子可以看到解释。
  • The source/target pair is common enough to warrant using the extra branch prediction capacity. This may increase the number of overall branch mispredictions, while improving the misprediction of indirect branches. The profitability is lower if the number of mispredicting branches is very large.

User/Source Coding Rule 1. (M impact, L generality) :
如果一个indirect branch(比如switch语句)有2个或多个常用的跳转目标,至少有一个与分支历史相关联,那么可以把这个跳转目标用条件分支替代。
Intel X86 优化指南阅读笔记--通用优化(前端)_第1张图片

Intel X86 优化指南阅读笔记--通用优化(前端)_第2张图片

Loop Unrolling

Loop Unrolling:循环展开,是一种牺牲程序的尺寸来加快程序的执行速度的优化方法。可以由程序员完成,也可由编译器自动优化完成。
循环展开最常用来降低循环开销,为具有多个功能单元的处理器提供指令级并行。也有利于指令流水线的调度。
induction variable:循环里那个递增递减变量。
common subexpression elimination:一种编译优化技术,它会搜索有没有等价表达式,是否值得用保存了计算结果的变量来替换它。
使用unrolling的好处:

  • 消除了每次循环的分支语句和操作induction variable。
  • 更加容易流水线处理来隐藏延时。
  • 把代码暴露给了其它优化器,例如对冗余load的移除,common subexpression elimination等。

使用unrolling的潜在坏处:

  • unrolling大的循环体,会导致代码大小增加。如果unrolling后的循环代码过大而不能保存在trace cache (TC)中,那将是有害的。
  • 如果循环体内有分支,unrolling后会增加对BTB的容量需求。

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)。
Intel X86 优化指南阅读笔记--通用优化(前端)_第3张图片
消除了分支语句来提高性能。

Fetch and Decode Optimization

Optimizing for Micro-fusion

一条指令它的操作数是一个寄存器和一个内存地址,另一条是该指令的操作数全是寄存器的版本,那么前者译码后的uop会比后者译码后的uop要多一些。但是如果如果要用寄存器-寄存器版本替换寄存器-内存版本,则需要2条连续指令,这很可能会降低取指令的吞吐量。

Assembly/Compiler Coding Rule 18. (ML impact, M generality):
为了提高取指令/译码吞吐量,如果内存版本的指令能从micro-fusion受益,那么优先使用这个版本,而不是全用寄存器的版本。

Optimizing for Macro-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。

Length-Changing Prefixes (LCP)

指令的长度最多可以到15字节。一些指令前缀可以动态改变指令长度,译码器必须能识别它。当predecoder遇到LCP时,必须使用一个较慢的译码算法,使predecoder译码从1个时钟周期上升到6个时钟周期。

能够动态改变指令长度的指令前缀包括:

  • Operand size prefix (0x66),后面的指令带有一个word/double立即数。使用场景可能是代码使用了16bit数据类型,unicode处理,和图像处理。
  • Address size prefix (0x67),后面的指令带有modr/m,运行在real, big real, 16-bit protected 或者 32-bit protected modes。使用场景可能是boot代码中。

Assembly/Compiler Coding Rule 21. (MH impact, MH generality):
产生代码时优先使用imm8或者imm32 代替 imm16。
如果一定要使用imm16,可以把高位填充0,装载到寄存器中,在使用该寄存器。

Double LCP Stalls
会产生LCP stall的指令,如果跨越了16字节取指令的边界,那么会触发两次LCP stall惩罚。如下的对齐情况会导致LCP stall惩罚两次。
Intel X86 优化指南阅读笔记--通用优化(前端)_第4张图片

  • 如果指令含有MODR/M和SIB部分,取指令的边界正好在MODR/M和SIB中间。
  • 如果指令起始地址在13字节处(一次取16字节的指令),并且通过寄存器和立即数来间接引用了内存位置。

为了避免LCP stalls两次惩罚,不要使用编码了SIB字节或者 addressing mode with byte displacement的LCP stall指令。

False LCP Stalls
False LCP stalls的特征跟LCP stalls相同,但是它发生在不带有imm16值得指令中。
发生条件为指令同时满足:

  • LCP指令,操作数为F7。
  • 在取指令的14字节偏移量处。
    这些指令为: not, neg, div, idiv, mul, 和imul。
    False LCP stalls会发生是因为无法知道指令长度,直到下一个16字节取出指令,下一个指令确定了长度。
    Assembly/Compiler Coding Rule 22. (M impact, ML generality):
    使用0xF7操作码的指令,不要放到取指令的14字节偏移处,避免使用这种指令到16bit数据,可以使用32bit数据类型。
Optimizing the Loop Stream Detector (LSD)

满足如下条件的循环,能够被LSD检测到,之后可以不再取指令,而是直接使用instruction queue中的uop,之后重复执行这个循环时,就不需要取指令,译码,而是直接使用队列中的uop。
LSD检测条件:

  • 必须少于等于4次取指令(每次16字节)
  • 必须少于等于18条指令。
  • 不能超过4个分支,不能有RET。
  • 通常迭代次数应该超过64次。

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.

Optimization for Decoded ICache

decoded ICache是从Sandy Bridge开始加入的新特性。它有两个优势:

  • 更高的uop吞吐量提供给乱序引擎。
  • 前端不需要译码从而节省了电力。

确保hot代码能保存到decoded ICache:

  • 确保hot代码块少于500条指令。特别的,如果循环体代码超过了500条指令,不要unroll它。
  • 如果循环体里有非常大的计算量,可以考虑把循环体分割成多个循环。
  • 如果应用能确保每个核心上只跑一个线程,那么可以认为hot代码大小大概为1000条指令。

Dense Read-Modify-Write Code
每个32字节对齐的内存块(代码段)最多只有18个uop能保存进Decoded ICache。因此,代码很密集时,可能会超过18个uop限制而不能加入Decoded ICache。 Read- modify-write (RMW) 指令就是这么个例子。

如下是一些可能的方案来使hot代码能够保存进Decoded ICache:

  • 用2条或3条指令来替换RMW指令,功能必须等价。
  • 对其代码,把密集部分打散到2个不同的32字节代码块里。
  • 循环里增加多个NOP指令,NOP在后端优化后不会真正执行。

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).

你可能感兴趣的:(Intel X86 优化指南阅读笔记--通用优化(前端))