通用的优化准则来充分利用超标量cpu的并行性:
- 遵循前端优化准则,最大化译码带宽和前端吞吐量。
- 最大化寄存器重命名带宽。本文将讨论包括如何正确处理partial registers, ROB read ports and instructions which causes side-effects on flags。
- 对一串指令的调度建议,这样可以保持多个独立的执行链可以同时存在于reservation station (RS),这样就可以充分利用后端的并行执行特性(超标量)
- 在执行后端,如何最小化延时。
不同指令会译码成不同个数的uop,所以编译器编译时选择指令时按照如下优先级:
Assembly/Compiler Coding Rule 28. (M impact, H generality):
优先选择译码成单一操作uop的指令,优先选择低延时的指令。编译器如果能很好地选择指令,那就不需要用户干预。
Assembly/Compiler Coding Rule 29. (M impact, L generality) :
Avoid prefixes, especially multiple non-0F-prefixed opcodes. (没理解)
Assembly/Compiler Coding Rule 30. (M impact, L generality):
不要使用太多segment寄存器。
Assembly/Compiler Coding Rule 31. (M impact, M generality):
避免使用复杂指令(比如enter,leave,或loop),他们会译码成超过四个uop,花费更多的时间。使用多个简单的指令序列替代他们。
Assembly/Compiler Coding Rule 32. (MH impact, M generality):
使用pop/push来管理用户栈,使用call/returns替代enter/leave。带有非0立即数参数的enter指令在流水线预测失败时会有比较大的延时。
没看懂
lea:只计算地址,不会把内存中的值移动到目的寄存器。
对于lea指令,Sandy Bridge微架构对比之前的微架构有两个显著地改变:
在基于NetBurst微架构的处理器上,LEA或者一系列LEA,ADD,SUB,SHIFT指令能替代变量与常量的乘法。
LEA指令也能用来替代多个连续的加法运算。例如:LEA ECX, [EAX + EBX + 4 + A]。此时LEA可以避免使用中间寄存器。这个用法也能减少代码大小。
Assembly/Compiler Coding Rule 33. (ML impact, L generality):
如果一条使用了缩放因子的LEA指令,在关键路径上执行,那么使用一系列ADD指令替代会更好。如果代码比较密集,大小超过了trace cache,那么就用LEA指令。
按位翻转指令,有通过CL寄存器,立即数和翻转1位三个版本指令。翻转1位版本速度比另外两个快,跟shift指令一样的延时。
Assembly/Compiler Coding Rule 34. (ML impact, L generality):
避免用通过CL寄存器,立即数这两个版本的按位翻转指令,使用翻转1位的指令替代。
Sandy Bridge微架构中,“ROL/ROR/SHL/SHR reg, cl”指令会被译码成3条uop。如果cl内容之后不需要,那么其中一条uop会被丢弃,从而提高性能。如果接下来的指令部分更新了cl结果(如果是全部更新,则可以省略一条uop),那么3条uop必须完整的执行。考虑如下循环的例子:
loop:
shl eax, cl
add ebx, eax
dec edx ; DEC does not update carry, causing SHL to execute slower three micro-ops flow
jnz loop
DEC指令并不修改cl,但是下一次迭代时shl eax, cl会修改,因此必须执行完整的3条uop。我么用SUB指令替代dec,它会全部的修改更新cl,所以shl指令只需要执行2条uop。
Addressing mode:地址模式定义了地址计算指令如何使用寄存器和常量。
计算地址时,使用地址模式下的指令而不是通用计算的指令。
注意到在Sandy Bridge微架构中,LEA指令如果使用多于2个操作数的版本,那么会降低速度。地址模式下的指令,使用base 和 index registers会消耗执行引擎中更多的读端口资源并可能遭遇更多的stall。软件需要选择最快速的地址计算指令版本。
在segmented model,一个段寄存器可能会作为指令的一个额外的操作数用来计算线性地址。
这一章没有搞明白,以下是我自己的理解:
Dependency Breaking Idioms指令,无论输入是什么都会有同样的结果,那么它就不依赖于寄存器原有值。我们可以改写原有指令序列,插入这类特定指令,就可以减少依赖,充分利用好乱序引擎。
当与0做比较时,优先考虑使用TEST指令。TSET优于AND,ADN指令多产生了一个额外的寄存器。TEST优于CMP …, 0,因为它的指令占空间比较小。
如果一条指令部分修改了flag register,接下来一条指令使用flag register,在这两条指令中间使用TEST指令可以帮助阻止partial flag register stall。
Assembly/Compiler Coding Rule 39. (ML impact, M generality) :
当逻辑AND结果不需要使用时,用TEST替代AND指令,这能在执行时解决uop。当CMP指令判断一个寄存器是否等于0时,使用TEST指令对比寄存器和它自己(搜索这两条指令说明即可明白)。避免对比常量和一个内存里的值,优先考虑先把内存值load后再对比常量和寄存器。
Assembly/Compiler Coding Rule 40. (ML impact, M generality) :
如果之前的算术运算已经设置了标志寄存器表明寄存器里的值是否为0,那么就不要再用额外的比较指令来判断寄存器是否为0,可以用恰当的条件jump指令。如果一定要做对比,用TEST指令。请确保选用指令时不要引入overflow问题。
NOP指令用来插入指令流,完成指令的一些对齐要求,类似于数据结构对齐填充。
软件要避免混合 integer/FP 操作作用于XMM寄存器,这会降低性能。
spill scheduling algorithm:当变量过多无法全部保存在寄存器中时,这个算法用来选择哪些变量保存到内存中。
Loop-carried dependence:dependence exists across iterations; i.e., if the loop is removed, the dependence no longer exists.
Loop-independent dependence:dependence exists within an iteration; i.e., if the loop is removed, the dependence still exists.
Assembly/Compiler Coding Rule 41. (H impact, MH generality):
For small loops, placing loop invariants in memory is better than spilling loop-carried dependencies. (没看明白)
在Ivy Bridge位架构中,一小部分register-to-register的移动指令能够在cpu前端就执行完,这个可以节约乱序引擎的资源。绝大多数这种移动指令可以做到0延时。
虽然执行引擎被优化为对常用操作快速执行,但是uop还是可能会遇到各种延时:
reorder buffer (ROB): Tomasulo algorithm用于乱序殷勤,ROB在这个算法的”Write Result”阶段,用来保存乱序执行指令的计算结果,这个buffer的内容可以被其他乱序指令使用。
reservation station(RS):允许CPU使用刚刚计算出来的数据,而不用等数据保存到寄存器中。
当uop经过rename后,需要确定其源操作数可能已经保存在了ROB,或者保存在了RS中,或者在旁路中(流水线输出结果旁路)。大多数情况下,源操作数是在RS中。ROB中的数据需要通过read port读取。
intel的微结构对RS做了优化,那么对于ROB中资源就可能不是所有的uop都能在同一个时钟周期读取里面的数据。
当不是所有的数据都能同时从ROB读取,这就产生了停顿,但是这个停顿很短,下一个时钟周期就会读到,完成rename过程,对外看来产生了一些rename带宽损失。
(优化策略没看明白,以后有缘再思考)
在这种情况下,只能考虑更换指令,从而使用不同的执行引擎的发射端口来减少竞争。
(没看明白)
通用寄存器可以部分更新,这就有可能产生stall。当一条指令引用一个寄存器,这个寄存器之前被部分修改过,那么就会产生一些stall。
使用部分更新寄存器,一般是为了节约寄存器,但是会造成指令的依赖,最简单的办法就是多用几个寄存器,而不是挤在这一个上。
XMM寄存器部分更新时也会有stall。采用如下建议解决:
当一条指令部分修改了flag register,紧接着下一条指令依赖这个flag register内容,那么就会产生“partial flag register stall”。
建议较少在例如: INC, DEC, SET CL 后面接例如 SHIFT CL指令。前者会写入部分flags,后者则条件依赖于flags。
对寄存器只写入部分可能会引入不期望的依赖关系。MOVSD REG, REG指令只写入寄存器的低64bit,如果有指令写入了该寄存器的搞64bit,那么这就产生了潜在的依赖关系。这将妨碍寄存器rename过程和减少指令并行度。
Assembly/Compiler Coding Rule 43. (M impact, ML generality):
避免使用部分修改寄存的操作,例如:MOVSD XMMREG1, XMMREG2。使用MOVAPD XMMREG1, XMMREG2 指令替代它(这个指令修改128bit寄存器)。
Assembly/Compiler Coding Rule 44. (ML impact, L generality):
Instead of using MOVUPD XMMREG1, MEM for a unaligned 128-bit load, use MOVSD XMMREG1, MEM; MOVSD XMMREG2, MEM+8; UNPCKLPD XMMREG1, XMMREG2. If the additional register is not available, then use MOVSD XMMREG1, MEM; MOVHPD XMMREG1, MEM+8.
Assembly/Compiler Coding Rule 45. (M impact, ML generality) :
Instead of using MOVUPD MEM, XMMREG1 for a store, use MOVSD MEM, XMMREG1; UNPCKHPD XMMREG1, XMMREG1; MOVSD MEM+8, XMMREG1 instead.