ove*_*eas 2 c++ x86 micro-optimization
这个问题是关于我们如何将整数与常数相乘.那么让我们看一个简单的函数:
int f(int x) {
return 10*x;
}
Run Code Online (Sandbox Code Playgroud)
如何最佳地优化该功能,尤其是在内联到呼叫者时?
方法1(由大多数优化编译器生成(例如,在Godbolt上))
lea (%rdi,%rdi,4), %eax
add %eax, %eax
Run Code Online (Sandbox Code Playgroud)
方法2(使用clang3.6和更早版本,使用-O3生成)
imul $10, %edi, %eax
Run Code Online (Sandbox Code Playgroud)
方法3(使用g ++ 6.2生成,无需优化,删除存储/重新加载)
mov %edi, %eax
sal $2, %eax
add %edi, %eax
add %eax, %eax
Run Code Online (Sandbox Code Playgroud)
哪个版本最快,为什么?主要对英特尔Haswell感兴趣.
根据Agner Fog的测试(以及像AIDA64这样的其他东西),自Core2以来,Intel CPU的imul r32,r32, imm延迟为3c,吞吐量为每1c一个.自Nehalem以来,64位乘法也快.(Agner表示Nehalem的imul r64,r64,imm速度较慢(2c吞吐量)imul r64,r64,但这与其他结果 不符.Instlatx64表示1c.)
Ryzen之前的AMD CPU速度较慢,例如,对于32位乘法,Steamroller的lat = 4c tput =每2c一个.对于64位乘法,lat = 6c tput =每4c一个.AMD Ryzen具有与英特尔相同的出色乘法性能.
在寻址模式下有2个组件的LEA(基本+索引,但没有恒定位移)在所有CPU上以1c延迟运行,除了可能用于LEA在管道的不同阶段运行的Atom(在实际的AGU中,而不是ALU)并且需要比"正常"ALU指令早4c输入.相反,我认为它的输入已经准备好,所以ADD可以使用相同周期的结果.(我没有对此进行测试,也没有任何Atom HW.)
在Intel SnB系列上,simple-LEA可以在端口1或5上运行,因此它的吞吐量是IMUL的两倍.
ADD可以在任何CPU上的任何ALU端口上运行.HSW引入了第4个ALU端口(与IvyBridge相比),因此每个时钟可以维持4个ALU uop(理论上).
因此LEA + ADD版本在大多数x86 CPU上具有2c延迟,而Haswell可以在每个时钟运行两次乘法.
但是,如果乘法只是较大周围环路的一小部分,这会影响总uop吞吐量,而不是乘法延迟或吞吐量,IMUL版本更好.
如果你的乘法常数对于两个LEA或SHL + LEA而言太大,那么你可能会更好地使用IMUL,特别是在主要用于具有极高性能整数乘法器的Intel CPU时.
SHL + LEA或SHL + SUB可能是由63乘有用例如(来自Godbolt: gcc6.2 -O3 -march=haswell)
movl %edi, %eax
sall $6, %eax
subl %edi, %eax
Run Code Online (Sandbox Code Playgroud)
在Haswell,其中MOV是零延迟,这只有2c延迟.但它是3个融合域uops,而1为imull $63, %edi, %eax.因此,管道中的uops更多,减少了CPU可以"看到"执行无序执行的程度.它还会增加uop缓存和L1 I-cache的压力,使编译器始终选择此策略,因为它的指令字节更多.
在IvyBridge之前的CPU上,这比IMUL严格更糟,除非其他东西竞争port1,因为它是3c延迟(MOV在关键路径依赖链上,并且具有1c延迟).
对于不同微体系结构中的相同周围代码,答案也会有所不同.