64-ia-32架构优化手册(16)

3.4.2.4. 优化循环流检测器(LSD)

在Intel Core微架构中,满足以下准则的循环由LSD检测,并从指令队列重播(replay)来供给解码器。

  • 必须不超过4个16字节的取指。
  • 必须不超过18条指令。
  • 可以包含不超过4被采用的分支,并且它们不能是RET。
  • 通常应该具有超过64个迭代。

在Intel微架构Nehalem中,这样改进循环流寄存器:

  • 在指令已解码队列(IDQ,参考2.5.2节)中缓冲已解码微操作来供给重命名/分配阶段。
  • LSD的大小被增加到28个微操作。

在Sandy Bridge与Haswell微架构中,LSD与微操作队列实现持续改进。它们具有如下特性:

表3-2. 由Sandy Bridge与Haswell微架构检测的小循环准则

Sandy Bridge与Ivy Bridge微架构

Haswell微架构

最多8个32指令字节块的取指

如果启用HTT,8块取指,如果关闭HTT,11块取指

最多28个微操作

如果启用HTT,28个微操作,如果关闭HTT,56个微操作

所有微操作位于已解码ICache(即DSB),但不来自MSROM

所有微操作位于DSB,包括来自MSROM的微操作

不超过8个被采用分支

不固定

不包括CALL与RET

不包括CALL与RET

不匹配的栈操作不适用

同左

许多计算密集循环,搜索以及字符串移动符合这些特征。这些循环超出了BPU预测能力,总是导致一个分支误预测。

Assembly/Compiler编程规则23.(影响MH,普遍性MH将一个循环长的指令序列分解为不超过LSD大小的短指令块的循环。

Assembly/Compiler编程规则24.(影响MH,普遍性M如果展开后的块超过LSD的大小,避免展开包含LCP暂停的循环。

3.4.2.5. 在Intel® Sandy Bridge中利用LSD微操作发布带宽

LSD持有构成小的“无限”循环的微操作。来自LSD的微操作在乱序引擎中分配。在LSD中的循环以一个回到循环开头的被采用分支结束。在循环末尾的被采用分支总是在该周期中最后分配的微操作。在循环开头的指令总是在下一个周期分配。如果代码性能受限于前端带宽,未使用的分配槽造成分配中的一个空泡,并导致性能下降。

在Intel微架构Sandy Bridge中,分配带宽是每周期4个微操作。当LSD中的微操作数导致最少未使用分配槽时,性能是最好的。你可以使用循环展开来控制在LSD中的微操作数。

在例子3-17里,代码对所有的数组元素求和。原始的代码每次迭代加一个元素。每次迭代有3个微操作,都在一个周期中分配。代码的吞吐率是每周期一个读。

在展开循环一次时,每次迭代有5个微操作,它们在2个周期中分配。代码的吞吐率仍然是每周期一个读。因此没有性能提高。

在展开循环两次时,每次迭代有7个微操作,仍然在2个周期中分配。因为在每个周期中可以执行两个读,这个代码具有每两个周期3个读操作的潜在吞吐率。

例子3-17. LSD中展开循环优化发布带宽

没有展开

展开一次

展开两次

lp: add eax, [rsi + 4* rcx]

dec rcx

jnz lp

lp: add eax, [rsi + 4* rcx]

add eax, [rsi + 4* rcx +4]

add rcx, -2

jnz lp

lp: add eax, [rsi + 4* rcx]

add eax, [rsi + 4* rcx +4]

add eax, [rsi + 4* rcx + 8]

add rcx, -3

jnz lp

3.4.2.6. 为已解码ICache优化

已解码ICache是Intel微架构Sandy Bridge的一个新特性。从已解码ICache运行代码有两个好处:

  • 乱序引擎更高的微操作供给带宽。
  • 前端不需要解码在已解码ICache中的代码。这节省了能源。

在已解码ICache与遗留解码流水线间切换需要开销。如果你的代码频繁地在前端与已解码ICache间切换,性能损失会超出仅从遗留流水线运行。

要确保“热”代码从已解码ICache供给:

  • 确保每块热代码少于500条指令。特别地,在一个循环中不要展开超过500条指令。这应该会激活已解码ICache存留,即使启用了超线程。
  • 对于在一个循环中有非常大块计算的应用程序,考虑循环分裂(loop-fission):将循环分裂为合适已解码ICache的多个循环,而不是会溢出单个循环。
  • 如果一个应用程序可以确定每核仅运行一个线程,它可以将热代码块大小增加到大约1000条指令。

稠密读-修改-写代码(dense read-modify-write code

已解码ICache对每32字节对齐内存块仅可以保持最多18个微操作。因此,以少量字节编码,但有许多微操作的指令高度集中的代码,可能超过18个微操作的限制,不能进入已解码ICache。读-修改-写(RMW)指令是这样指令的一个良好例子。

RMW指令接受一个内存源操作数,一个寄存器源操作数,并使用内存源操作数作为目标。相同的功能可以两到三条指令实现:第一条读内存源操作数,第二条使用第二个寄存器源操作数执行该操作,最后的指令将结果写回内存。这些指令通常导致相同数量的微操作,但使用更多的字节来编码相同的功能。

一个可以广泛使用RMW指令的情形是当编译器进取地优化代码大小时。

下面是将热代码放入已解码ICache的某些可能解决方案:

  • 以2到3条功能相同的指令替换RMW指令。例如,“adc [rdi], rcx”只有3字节长;等效的序列“adc rax, [rdi]”+“mov [rdi], rax”是6个字节。
  • 对齐代码,使稠密部分被分开到两个32字节块。在使用一个自动对齐代码的工具时,这个解决方案是有用的,并且不关心代码改变。
  • 通过在循环中添加多字节NOP展开代码。注意这个解决方案对执行添加了微操作。

为已解码ICache对齐无条件分支

对于进入已解码ICache的代码,每个无条件分支是占据一个已解码ICache通道(way)的最后微操作。因此,每32字节对齐块仅3个无条件分支可以进入已解码ICache。

在跳转表与switch声明中无条件分支是频繁的。下面是这些构造的例子,以编写它们的方法,使它们可以放入已解码ICache。

编译器为C++虚拟函数或DLL分发表创建跳转表。每个无条件分支消耗5字节;因此最多7个无条件分支可与一个32字节块关联。这样,如果在每个32字节对齐块中无条件分支太稠密,跳转表可能不能放入已解码ICache。这会导致在分支表前后执行的代码的性能下降。

解决方案是在分支表的分支中添加多个字节NOP指令。这可能会增加代码大小,小心使用。不过,这些NOP不会执行,因此在后面的流水线阶段中不会有性能损失。

Switch-case构造代表了一个类似的情形。一个case条件的求值导致一个无条件分支。在放入一个对齐32字节块的每3条连续无条件分支上可以应用相同的多字节NOP方法。

在一个已解码ICache通道中的两个分支

已解码ICache可以在一个通道在保持两个分支。在一个32字节对齐块中稠密的分支,或它们与其他指令的次序,可能阻止该块中指令所有的微操作进入已解码ICache。这不经常发生。当它发生时,你可以在代码合适的地方放置NOP指令。要确保这些NOP指令表示热代码的部分。

Assembly/Compiler编程规则25.(影响M,普遍性M避免在一个栈操作序列(POP,PUSH,CALL,RET)中放入对ESP的显式引用。

3.4.2.7. 其他解码指引

Assembly/Compiler编程规则26.(影响ML,普遍性L使用小于8字节长度的简单指令。

Assembly/Compiler编程规则27.(影响M,普遍性MH避免使用改变立即数及位移(displacement)大小的前缀。

长指令(超过7字节)会限制每周期解码指令数。每个前缀增加指令长度1字节,可能会限制解码器的吞吐率。另外,多前缀仅能由第一个解码器解码。这些前缀也导致了解码时的时延。如果不能避免改变立即数或位移大小多前缀或一个前缀,在因为其他原因暂停流水线的指令后调度它们。

猜你喜欢

转载自blog.csdn.net/wuhui_gdnt/article/details/82015427