使用constant - imul或shl-add-combination进行乘法运算

时间:2016-11-12 14:47:15

标签: c++ x86 micro-optimization

这个问题是关于我们如何将整数与常数相乘。让我们来看一个简单的函数:

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感兴趣。

3 个答案:

答案 0 :(得分:5)

根据Agner Fog's testing(以及AIDA64等其他内容),自Core2以来,英特尔CPU的{1}延迟为3c,吞吐量为每1c一个。自Nehalem以来,64位乘法也快。 (Agner说Nehalem imul r32,r32, immimul r64,r64,imm更慢(2c吞吐量),但这与其他结果不符。Instlatx64 says 1c.

在Ryzen之前的AMD CPU速度较慢,例如对于32位乘法,压路机的lat = 4c tput =每2c一个。对于64位乘法,lat = 6c tput =每4c一个。 AMD Ryzen具有与英特尔相同的出色乘法性能。

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延迟)。

像往常一样,没有一个asm片段可以说对所有情况都是最佳的。这取决于周围代码中的瓶颈:延迟,吞吐量或uops。

对于不同微体系结构中的相同周围代码,答案也会有所不同。

答案 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上看到(它有一些模板魔法,但归结为非常简单的代码,已经多次检查过)。请注意,我删除了计时器和其他一些东西,这对于检查程序集是不必要的。