这个问题是关于我们如何将整数与常数相乘。让我们来看一个简单的函数:
int f(int x) {
return 10*x;
}
如何最佳地优化该功能,尤其是在内联到呼叫者时?
方法1 (由大多数优化编译器生成(例如on Godbolt))
lea (%rdi,%rdi,4), %eax
add %eax, %eax
方法2 (使用clang3.6及更早版本,使用-O3制作)
imul $10, %edi, %eax
方法3 (使用g ++ 6.2生成,无需优化,删除存储/重新加载)
mov %edi, %eax
sal $2, %eax
add %edi, %eax
add %eax, %eax
哪个版本最快,为什么?主要对英特尔Haswell感兴趣。
答案 0 :(得分:5)
根据Agner Fog's testing(以及AIDA64等其他内容),自Core2以来,英特尔CPU的{1}延迟为3c,吞吐量为每1c一个。自Nehalem以来,64位乘法也快。 (Agner说Nehalem imul r32,r32, imm
比imul r64,r64,imm
更慢(2c吞吐量),但这与其他结果不符。Instlatx64 says 1c.)
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微处理(理论上)。
所以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
)
imul r64,r64
在Haswell,其中MOV为零延迟,这只有2c延迟。但它是3个融合域uops与 movl %edi, %eax
sall $6, %eax
subl %edi, %eax
的1。因此,管道中的uops更多,减少了CPU可以“看到”执行无序执行的程度。它还会增加uop缓存和L1 I-cache的压力,让编译器始终选择这种策略,因为它的指令字节更多。
在IvyBridge之前的CPU上,除非其他东西竞争port1,否则这比IMUL严重得多,因为它是3c延迟(MOV在关键路径依赖链上,并且具有1c延迟)。
对于不同微体系结构中的相同周围代码,答案也会有所不同。
答案 1 :(得分:1)
我猜测移位和添加序列比imul更快;许多版本的x86芯片都是如此。我不知道Haswell是否属实;如果可行的话,在2个时钟周期内进行imul会占用大量的芯片资源。
我有点惊讶它没有产生更快的序列:
lea y, [2*y]
lea y, [5*y]
[OP编辑他的答案,显示优化代码生成ADD然后LEA。是的,这是一个更好的答案; ADD r,r在空间上小于lea .. [2 * y]因此得到的代码更小且速度相同]
答案 2 :(得分:1)
我刚做了一些测量。我们使用问题中给出的说明模拟汇编中的以下代码:
for (unsigned i = 0; i < (1 << 30); ++i) {
r1 = r2 * 10;
r2 = r1 * 10;
}
临时工可能需要一些额外的寄存器。
注意:所有测量都是每次乘法的周期。
我们使用clang编译器和-O3,因为我们完全得到了我们想要的程序集(gcc有时会在循环中添加很少的MOV)。我们有两个参数:u = #unrolled loops,i = #ilp。例如,对于u = 4,i = 2,我们得到以下结果:
401d5b: 0f a2 cpuid
401d5d: 0f 31 rdtsc
401d5f: 89 d6 mov %edx,%esi
401d61: 89 c7 mov %eax,%edi
401d63: 41 89 f0 mov %esi,%r8d
401d66: 89 f8 mov %edi,%eax
401d68: b9 00 00 20 00 mov $0x200000,%ecx
401d6d: 0f 1f 00 nopl (%rax)
401d70: 6b d5 0a imul $0xa,%ebp,%edx
401d73: 41 6b f7 0a imul $0xa,%r15d,%esi
401d77: 6b fa 0a imul $0xa,%edx,%edi
401d7a: 6b d6 0a imul $0xa,%esi,%edx
401d7d: 6b f7 0a imul $0xa,%edi,%esi
401d80: 6b fa 0a imul $0xa,%edx,%edi
401d83: 6b d6 0a imul $0xa,%esi,%edx
401d86: 6b f7 0a imul $0xa,%edi,%esi
401d89: 6b fa 0a imul $0xa,%edx,%edi
401d8c: 6b d6 0a imul $0xa,%esi,%edx
401d8f: 6b f7 0a imul $0xa,%edi,%esi
401d92: 6b fa 0a imul $0xa,%edx,%edi
401d95: 44 6b e6 0a imul $0xa,%esi,%r12d
401d99: 44 6b ef 0a imul $0xa,%edi,%r13d
401d9d: 41 6b ec 0a imul $0xa,%r12d,%ebp
401da1: 45 6b fd 0a imul $0xa,%r13d,%r15d
401da5: ff c9 dec %ecx
401da7: 75 c7 jne 401d70 <_Z7measureIN5timer5rtdscE2V1Li16777216ELi4ELi2EEvv+0x130>
401da9: 49 c1 e0 20 shl $0x20,%r8
401dad: 49 09 c0 or %rax,%r8
401db0: 0f 01 f9 rdtscp
请注意,这些不是8,而是16个imul指令,因为这是r2 = r1 * 10; r1 = r2 * 10;我只会发布主循环,即
401d70: 6b d5 0a imul $0xa,%ebp,%edx
401d73: 41 6b f7 0a imul $0xa,%r15d,%esi
401d77: 6b fa 0a imul $0xa,%edx,%edi
401d7a: 6b d6 0a imul $0xa,%esi,%edx
401d7d: 6b f7 0a imul $0xa,%edi,%esi
401d80: 6b fa 0a imul $0xa,%edx,%edi
401d83: 6b d6 0a imul $0xa,%esi,%edx
401d86: 6b f7 0a imul $0xa,%edi,%esi
401d89: 6b fa 0a imul $0xa,%edx,%edi
401d8c: 6b d6 0a imul $0xa,%esi,%edx
401d8f: 6b f7 0a imul $0xa,%edi,%esi
401d92: 6b fa 0a imul $0xa,%edx,%edi
401d95: 44 6b e6 0a imul $0xa,%esi,%r12d
401d99: 44 6b ef 0a imul $0xa,%edi,%r13d
401d9d: 41 6b ec 0a imul $0xa,%r12d,%ebp
401da1: 45 6b fd 0a imul $0xa,%r13d,%r15d
401da5: ff c9 dec %ecx
401da7: 75 c7 jne 401d70 <_Z7measureIN5timer5rtdscE2V1Li16777216ELi4ELi2EEvv+0x130>
我们使用perf(PERF_COUNT_HW_REF_CPU_CYCLES =“ref cycles”和PERF_COUNT_HW_CPU_CYCLES =“cpu cycles”)代替rtdsc。
我们修正u = 16,并且变化i = {1,2,4,8,16,32}。我们得到
name uroll ilp ref cyclescpu cyclesp0 p1 p2 p3 p4 p5 p6 p7
V1 16 1 2.723 3.006 0.000 1.000 0.000 0.000 0.000 0.000 0.031 0.000
V1 16 2 1.349 1.502 0.000 1.000 0.000 0.000 0.000 0.000 0.016 0.000
V1 16 4 0.902 1.002 0.000 1.000 0.000 0.000 0.000 0.000 0.008 0.000
V1 16 8 0.899 1.001 0.000 1.000 0.004 0.006 0.008 0.002 0.006 0.002
V1 16 16 0.898 1.001 0.000 1.000 0.193 0.218 0.279 0.001 0.003 0.134
V1 16 32 0.926 1.008 0.000 1.004 0.453 0.490 0.642 0.001 0.002 0.322
这是有道理的。 ref循环可以忽略。
右侧的列显示执行端口上的微操作数。我们在p1上有一条指令(当然是imul),在p6上我们有减量(在第一种情况下为1/16)。之后我们还可以看到,由于强大的套准压力,我们得到了其他微型机。
好的,让我们转到第二个版本,现在是:
402270: 89 ea mov %ebp,%edx
402272: c1 e2 02 shl $0x2,%edx
402275: 01 ea add %ebp,%edx
402277: 01 d2 add %edx,%edx
402279: 44 89 fe mov %r15d,%esi
40227c: c1 e6 02 shl $0x2,%esi
40227f: 44 01 fe add %r15d,%esi
402282: 01 f6 add %esi,%esi
402284: 89 d7 mov %edx,%edi
402286: c1 e7 02 shl $0x2,%edi
402289: 01 d7 add %edx,%edi
40228b: 01 ff add %edi,%edi
40228d: 89 f2 mov %esi,%edx
40228f: c1 e2 02 shl $0x2,%edx
402292: 01 f2 add %esi,%edx
402294: 01 d2 add %edx,%edx
402296: 89 fe mov %edi,%esi
402298: c1 e6 02 shl $0x2,%esi
40229b: 01 fe add %edi,%esi
40229d: 01 f6 add %esi,%esi
40229f: 89 d7 mov %edx,%edi
4022a1: c1 e7 02 shl $0x2,%edi
4022a4: 01 d7 add %edx,%edi
4022a6: 01 ff add %edi,%edi
4022a8: 89 f2 mov %esi,%edx
4022aa: c1 e2 02 shl $0x2,%edx
4022ad: 01 f2 add %esi,%edx
4022af: 01 d2 add %edx,%edx
4022b1: 89 fe mov %edi,%esi
4022b3: c1 e6 02 shl $0x2,%esi
4022b6: 01 fe add %edi,%esi
4022b8: 01 f6 add %esi,%esi
4022ba: 89 d7 mov %edx,%edi
4022bc: c1 e7 02 shl $0x2,%edi
4022bf: 01 d7 add %edx,%edi
4022c1: 01 ff add %edi,%edi
4022c3: 89 f2 mov %esi,%edx
4022c5: c1 e2 02 shl $0x2,%edx
4022c8: 01 f2 add %esi,%edx
4022ca: 01 d2 add %edx,%edx
4022cc: 89 fe mov %edi,%esi
4022ce: c1 e6 02 shl $0x2,%esi
4022d1: 01 fe add %edi,%esi
4022d3: 01 f6 add %esi,%esi
4022d5: 89 d7 mov %edx,%edi
4022d7: c1 e7 02 shl $0x2,%edi
4022da: 01 d7 add %edx,%edi
4022dc: 01 ff add %edi,%edi
4022de: 89 f2 mov %esi,%edx
4022e0: c1 e2 02 shl $0x2,%edx
4022e3: 01 f2 add %esi,%edx
4022e5: 01 d2 add %edx,%edx
4022e7: 89 fe mov %edi,%esi
4022e9: c1 e6 02 shl $0x2,%esi
4022ec: 01 fe add %edi,%esi
4022ee: 01 f6 add %esi,%esi
4022f0: 89 d5 mov %edx,%ebp
4022f2: c1 e5 02 shl $0x2,%ebp
4022f5: 01 d5 add %edx,%ebp
4022f7: 01 ed add %ebp,%ebp
4022f9: 41 89 f7 mov %esi,%r15d
4022fc: 41 c1 e7 02 shl $0x2,%r15d
402300: 41 01 f7 add %esi,%r15d
402303: 45 01 ff add %r15d,%r15d
402306: ff c8 dec %eax
402308: 0f 85 62 ff ff ff jne 402270 <_Z7measureIN5timer5rtdscE2V2Li16777216ELi4ELi2EEvv+0xe0>
V2的测量
name uroll ilp ref cyclescpu cyclesp0 p1 p2 p3 p4 p5 p6 p7
V2 16 1 2.696 3.004 0.776 0.714 0.000 0.000 0.000 0.731 0.811 0.000
V2 16 2 1.454 1.620 0.791 0.706 0.000 0.000 0.000 0.719 0.800 0.000
V2 16 4 0.918 1.022 0.836 0.679 0.000 0.000 0.000 0.696 0.795 0.000
V2 16 8 0.914 1.018 0.864 0.647 0.006 0.002 0.012 0.671 0.826 0.008
V2 16 16 1.277 1.414 0.834 0.652 0.237 0.263 0.334 0.685 0.887 0.161
V2 16 32 1.572 1.751 0.962 0.703 0.532 0.560 0.671 0.740 1.003 0.230
这也有意义,我们速度较慢,并且在i = 32的情况下,我们肯定会有太大的寄存器压力(由使用的其他端口和组件中显示)...如果i = 0,我们可以验证,那个p0 + p1 + p5 + p6 = ~3.001,所以当然没有ILP。我们可以期待4个cpu周期,但MOV是免费的(寄存器分配)。
当i = 4:SHL在p0或p6上执行时,ADD和MOV都在p0,1,5或6上执行。因此我们在p0或p6上有1个操作,并且2 + 1操作(添加) / mov)在p0,p1,p5或p6上。同样,MOV是免费的,所以我们每次乘法得到1个循环。
最后使用优化版本:
402730: 67 8d 7c ad 00 lea 0x0(%ebp,%ebp,4),%edi
402735: 01 ff add %edi,%edi
402737: 67 43 8d 2c bf lea (%r15d,%r15d,4),%ebp
40273c: 01 ed add %ebp,%ebp
40273e: 67 8d 1c bf lea (%edi,%edi,4),%ebx
402742: 01 db add %ebx,%ebx
402744: 67 8d 7c ad 00 lea 0x0(%ebp,%ebp,4),%edi
402749: 01 ff add %edi,%edi
40274b: 67 8d 2c 9b lea (%ebx,%ebx,4),%ebp
40274f: 01 ed add %ebp,%ebp
402751: 67 8d 1c bf lea (%edi,%edi,4),%ebx
402755: 01 db add %ebx,%ebx
402757: 67 8d 7c ad 00 lea 0x0(%ebp,%ebp,4),%edi
40275c: 01 ff add %edi,%edi
40275e: 67 8d 2c 9b lea (%ebx,%ebx,4),%ebp
402762: 01 ed add %ebp,%ebp
402764: 67 8d 1c bf lea (%edi,%edi,4),%ebx
402768: 01 db add %ebx,%ebx
40276a: 67 8d 7c ad 00 lea 0x0(%ebp,%ebp,4),%edi
40276f: 01 ff add %edi,%edi
402771: 67 8d 2c 9b lea (%ebx,%ebx,4),%ebp
402775: 01 ed add %ebp,%ebp
402777: 67 8d 1c bf lea (%edi,%edi,4),%ebx
40277b: 01 db add %ebx,%ebx
40277d: 67 44 8d 64 ad 00 lea 0x0(%ebp,%ebp,4),%r12d
402783: 45 01 e4 add %r12d,%r12d
402786: 67 44 8d 2c 9b lea (%ebx,%ebx,4),%r13d
40278b: 45 01 ed add %r13d,%r13d
40278e: 67 43 8d 2c a4 lea (%r12d,%r12d,4),%ebp
402793: 01 ed add %ebp,%ebp
402795: 67 47 8d 7c ad 00 lea 0x0(%r13d,%r13d,4),%r15d
40279b: 45 01 ff add %r15d,%r15d
40279e: ff c9 dec %ecx
4027a0: 75 8e jne 402730 <_Z7measureIN5timer5rtdscE2V3Li16777216ELi4ELi2EEvv+0x170>
V3的测量
name uroll ilp ref cyclescpu cyclesp0 p1 p2 p3 p4 p5 p6 p7
V3 16 1 1.797 2.002 0.447 0.558 0.000 0.000 0.000 0.557 0.469 0.000
V3 16 2 1.273 1.418 0.448 0.587 0.000 0.000 0.000 0.528 0.453 0.000
V3 16 4 0.774 0.835 0.449 0.569 0.000 0.000 0.000 0.548 0.442 0.000
V3 16 8 0.572 0.636 0.440 0.555 0.017 0.021 0.032 0.562 0.456 0.012
V3 16 16 0.753 0.838 0.433 0.630 0.305 0.324 0.399 0.644 0.458 0.165
V3 16 32 0.976 1.087 0.467 0.766 0.514 0.536 0.701 0.737 0.458 0.333
好的,现在我们比imul更快。 i = 0的2个周期(两个指令都是1),而对于i = 8,我们甚至更快: lea可以在p1和p5上执行,如上所述,在p0,p1,p5或p6上执行。因此,如果完美安排,LEA进入p1和p5,ADD进入p0或p6。不幸的是,在这种情况下它并不完美(组装很好)。我认为调度并不完美,并且p1 / p5端口上有少量ADD接地。
所有代码都可以在gcc.godbolt.org上看到(它有一些模板魔法,但归结为非常简单的代码,已经多次检查过)。请注意,我删除了计时器和其他一些东西,这对于检查程序集是不必要的。