常量乘法 - imul 或 shl-add-combination

Multiplication with constant - imul or shl-add-combination

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

int f(int x) {
    return 10*x;
}

如何才能最好地优化该函数,尤其是内联到调用方时?

方法 1(由大多数优化编译器生成(例如 on Godbolt))

    lea    (%rdi,%rdi,4), %eax
    add    %eax, %eax

方法 2(使用 clang3.6 及更早版本生成,使用 -O3)

    imul   , %edi, %eax

方法 3(使用 g++6.2 生成,未优化,移除 stores/reloads)

    mov    %edi, %eax
    sal    , %eax
    add    %edi, %eax
    add    %eax, %eax

哪个版本最快,为什么?主要对 Intel Haswell 感兴趣。

我猜移位加序列比 imul 快;许多版本的 x86 芯片都是如此。我不知道 Haswell 是不是这样;尽管如此,如果完全可行的话,在 2 个时钟周期内执行 imul 会占用大量芯片资源。

我有点惊讶它没有产生更快的序列:

 lea   y, [2*y]
 lea   y, [5*y]

[OP 编辑​​他的答案,显示生成 ADD 然后生成 LEA 的优化代码。是的,这是一个更好的答案; ADD r,r 在空间上比 lea ..[2*y] 更小,因此生成的代码更小且速度相同]

我刚刚做了一些测量。我们使用问题中给出的说明在汇编中模拟以下代码:

    for (unsigned i = 0; i < (1 << 30); ++i) {
        r1 = r2 * 10;
        r2 = r1 * 10;
    }

可能临时需要一些额外的寄存器。

注意:所有测量值都在 周期中每一次乘法

我们使用带 -O3 的 clang 编译器,因为在那里我们准确地得到了我们想要的程序集(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    [=11=]x200000,%ecx
  401d6d:   0f 1f 00                nopl   (%rax)
  401d70:   6b d5 0a                imul   [=11=]xa,%ebp,%edx
  401d73:   41 6b f7 0a             imul   [=11=]xa,%r15d,%esi
  401d77:   6b fa 0a                imul   [=11=]xa,%edx,%edi
  401d7a:   6b d6 0a                imul   [=11=]xa,%esi,%edx
  401d7d:   6b f7 0a                imul   [=11=]xa,%edi,%esi
  401d80:   6b fa 0a                imul   [=11=]xa,%edx,%edi
  401d83:   6b d6 0a                imul   [=11=]xa,%esi,%edx
  401d86:   6b f7 0a                imul   [=11=]xa,%edi,%esi
  401d89:   6b fa 0a                imul   [=11=]xa,%edx,%edi
  401d8c:   6b d6 0a                imul   [=11=]xa,%esi,%edx
  401d8f:   6b f7 0a                imul   [=11=]xa,%edi,%esi
  401d92:   6b fa 0a                imul   [=11=]xa,%edx,%edi
  401d95:   44 6b e6 0a             imul   [=11=]xa,%esi,%r12d
  401d99:   44 6b ef 0a             imul   [=11=]xa,%edi,%r13d
  401d9d:   41 6b ec 0a             imul   [=11=]xa,%r12d,%ebp
  401da1:   45 6b fd 0a             imul   [=11=]xa,%r13d,%r15d
  401da5:   ff c9                   dec    %ecx
  401da7:   75 c7                   jne    401d70 <_Z7measureIN5timer5rtdscE2V1Li16777216ELi4ELi2EEvv+0x130>
  401da9:   49 c1 e0 20             shl    [=11=]x20,%r8
  401dad:   49 09 c0                or     %rax,%r8
  401db0:   0f 01 f9                rdtscp 

注意这里不是8条,而是16条imul指令,因为这是r2 = r1 * 10; r1 = r2 * 10;我只会 post 主循环,即

  401d70:   6b d5 0a                imul   [=12=]xa,%ebp,%edx
  401d73:   41 6b f7 0a             imul   [=12=]xa,%r15d,%esi
  401d77:   6b fa 0a                imul   [=12=]xa,%edx,%edi
  401d7a:   6b d6 0a                imul   [=12=]xa,%esi,%edx
  401d7d:   6b f7 0a                imul   [=12=]xa,%edi,%esi
  401d80:   6b fa 0a                imul   [=12=]xa,%edx,%edi
  401d83:   6b d6 0a                imul   [=12=]xa,%esi,%edx
  401d86:   6b f7 0a                imul   [=12=]xa,%edi,%esi
  401d89:   6b fa 0a                imul   [=12=]xa,%edx,%edi
  401d8c:   6b d6 0a                imul   [=12=]xa,%esi,%edx
  401d8f:   6b f7 0a                imul   [=12=]xa,%edi,%esi
  401d92:   6b fa 0a                imul   [=12=]xa,%edx,%edi
  401d95:   44 6b e6 0a             imul   [=12=]xa,%esi,%r12d
  401d99:   44 6b ef 0a             imul   [=12=]xa,%edi,%r13d
  401d9d:   41 6b ec 0a             imul   [=12=]xa,%r12d,%ebp
  401da1:   45 6b fd 0a             imul   [=12=]xa,%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

这是有道理的。可以忽略引用循环。

右边的列显示了执行端口上的微操作数。我们在 p1 上有一条指令(当然是 imul),在 p6 上我们有减量(第一种情况下为 1/16)。稍后我们还可以看到,由于强大的寄存器压力,我们得到了其他微指令。

好的,让我们转到第二个版本,现在是:

  402270:   89 ea                   mov    %ebp,%edx
  402272:   c1 e2 02                shl    [=14=]x2,%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    [=14=]x2,%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    [=14=]x2,%edi
  402289:   01 d7                   add    %edx,%edi
  40228b:   01 ff                   add    %edi,%edi
  40228d:   89 f2                   mov    %esi,%edx
  40228f:   c1 e2 02                shl    [=14=]x2,%edx
  402292:   01 f2                   add    %esi,%edx
  402294:   01 d2                   add    %edx,%edx
  402296:   89 fe                   mov    %edi,%esi
  402298:   c1 e6 02                shl    [=14=]x2,%esi
  40229b:   01 fe                   add    %edi,%esi
  40229d:   01 f6                   add    %esi,%esi
  40229f:   89 d7                   mov    %edx,%edi
  4022a1:   c1 e7 02                shl    [=14=]x2,%edi
  4022a4:   01 d7                   add    %edx,%edi
  4022a6:   01 ff                   add    %edi,%edi
  4022a8:   89 f2                   mov    %esi,%edx
  4022aa:   c1 e2 02                shl    [=14=]x2,%edx
  4022ad:   01 f2                   add    %esi,%edx
  4022af:   01 d2                   add    %edx,%edx
  4022b1:   89 fe                   mov    %edi,%esi
  4022b3:   c1 e6 02                shl    [=14=]x2,%esi
  4022b6:   01 fe                   add    %edi,%esi
  4022b8:   01 f6                   add    %esi,%esi
  4022ba:   89 d7                   mov    %edx,%edi
  4022bc:   c1 e7 02                shl    [=14=]x2,%edi
  4022bf:   01 d7                   add    %edx,%edi
  4022c1:   01 ff                   add    %edi,%edi
  4022c3:   89 f2                   mov    %esi,%edx
  4022c5:   c1 e2 02                shl    [=14=]x2,%edx
  4022c8:   01 f2                   add    %esi,%edx
  4022ca:   01 d2                   add    %edx,%edx
  4022cc:   89 fe                   mov    %edi,%esi
  4022ce:   c1 e6 02                shl    [=14=]x2,%esi
  4022d1:   01 fe                   add    %edi,%esi
  4022d3:   01 f6                   add    %esi,%esi
  4022d5:   89 d7                   mov    %edx,%edi
  4022d7:   c1 e7 02                shl    [=14=]x2,%edi
  4022da:   01 d7                   add    %edx,%edi
  4022dc:   01 ff                   add    %edi,%edi
  4022de:   89 f2                   mov    %esi,%edx
  4022e0:   c1 e2 02                shl    [=14=]x2,%edx
  4022e3:   01 f2                   add    %esi,%edx
  4022e5:   01 d2                   add    %edx,%edx
  4022e7:   89 fe                   mov    %edi,%esi
  4022e9:   c1 e6 02                shl    [=14=]x2,%esi
  4022ec:   01 fe                   add    %edi,%esi
  4022ee:   01 f6                   add    %esi,%esi
  4022f0:   89 d5                   mov    %edx,%ebp
  4022f2:   c1 e5 02                shl    [=14=]x2,%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    [=14=]x2,%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个操作(add/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 上执行,add 如上所述,可以在 p0、p1、p5 或 p6 上执行。因此,如果安排得当,LEA 会转到 p1 和 p5,ADD 会转到 p0 或 p6。不幸的是,在这种情况下它并不是那么完美(组装很好)。我想调度并不完美,一些 ADD 落在 p1/p5 端口上。

所有代码都可以在gcc.godbolt.org上看到(它有一些模板魔力,但归结为非常简单的代码,已经检查了很多次)。请注意,我删除了计时器和其他一些东西,这对于检查程序集不是必需的。

根据Agner Fog's testing (and other stuff like AIDA64) Intel CPUs since Core2 have had imul r32,r32, imm latency of 3c, throughput one per 1c. Since Nehalem, 64-bit multiplies are also that fast. (Agner says Nehalem's imul r64,r64,imm slower (2c throughput) than imul r64,r64, but that doesn't match other results. Instlatx64 says 1c.)

AMD CPUs 在 Ryzen 之前更慢,例如对于 32 位乘法,Steamroller 有 lat=4c tput=one per 2c。对于 64 位乘法,lat=6c tput=one per 4c。 AMD Ryzen 拥有与 Intel 一样出色的乘法性能


LEA 在寻址模式下有 2 个组件(基址 + 索引,但没有恒定位移)运行s 在所有 Intel 上的 1c 延迟 CPUs1,除了 Atom,其中 LEA 运行s 在管道的不同阶段(在实际的 AGU 中,而不是 ALU 中)并且需要其输入比“正常”ALU 指令早 4c 就绪。相反,我认为它的输入会更快准备好,因此 ADD 可以使用相同周期的结果。 (我没有测试过这个,也没有任何 Atom 硬件。)

在 Intel SnB 系列上,simple-LEA 可以在端口 1 或 5 上 运行,因此它的吞吐量是 IMUL 的两倍。

ADD 可以在任何 CPU 上的任何 ALU 端口上 运行。 HSW 引入了第 4 个 ALU 端口(相对于 IvyBridge),因此它每个时钟可以维持 4 个 ALU 微指令(理论上)。

因此 LEA+ADD 版本在大多数 x86 CPUs 上有 2c 延迟,而在 Haswell 上可以运行 每个时钟两次乘法。

脚注 1:在 AMD(包括 Zen/Zen2)上,缩放索引使 LEA“变慢”(2 个周期延迟和更少端口上的 运行s)。例如lea r32, [r64+r64*2] 在 Zen / Zen2 的 2 个周期延迟 on Zen2 vs. 1 cycle on Skylake. (Agner Fog also mentions that lea r32, [r64...] is slower on AMD, but that might only have been a Bulldozer effect; it's not apparent in https://uops.info/'s 结果下测量。)


但是,如果乘法只是 更大的周围循环的一小部分,它会限制总 uop 吞吐量,而不是乘法延迟或吞吐量,则 IMUL 版本更好。


如果您的乘法常数对于两个 LEA 或一个 SHL+LEA 来说太大,那么您最好使用 IMUL,尤其是在主要针对具有极高性能整数的英特尔 CPU 进行调整时乘数。

SHL+LEA 或 SHL+SUB 可能有用,例如乘以 63。(来自 Godbolt:gcc6.2 -O3 -march=haswell

    movl    %edi, %eax
    sall    , %eax
    subl    %edi, %eax

在 Haswell 上,MOV 是零延迟的,这只有 2c 延迟。但它是 3 个融合域微指令,而 imull , %edi, %eax 是 1 个。所以它在管道中有更多的微指令,减少了 CPU 可以“看到”乱序执行的距离。它还增加了 uop 缓存和 L1 I-cache 的压力,因为编译器会始终选择这种策略,因为它有更多的指令字节。

在 IvyBridge 之前的 CPUs 上,这比 IMUL 严格地差,除非有其他东西在竞争端口 1,因为它是 3c 延迟(MOV 在关键路径依赖链上,并且有 1c 延迟)。

和往常一样,none 的 asm 片段可以说是所有情况下的最佳选择。这取决于周围代码中的瓶颈是什么:延迟、吞吐量或 uops。

相同的周边代码在不同的微架构上的答案也会不同。