Gcc 缩放索引寻址模式是个好主意吗?

Gcc 缩放索引寻址模式是个好主意吗?,gcc,assembly,clang,x86-64,micro-optimization,Gcc,Assembly,Clang,X86 64,Micro Optimization,考虑以下代码: void foo(int* __restrict__ a) { int i; int val = 0; for (i = 0; i < 100; i++) { val = 2 * i; a[i] = val; } } clang 5.0: foo(int*): # @foo(int*) xor eax, eax .LBB0_1: # =>This Inner Loop Header: Depth=1 m

考虑以下代码:

void foo(int* __restrict__ a)
{
    int i; int val = 0;
    for (i = 0; i < 100; i++) {
        val = 2 * i;
        a[i] = val;
    }
}
clang 5.0:

foo(int*): # @foo(int*)
  xor eax, eax
.LBB0_1: # =>This Inner Loop Header: Depth=1
  mov dword ptr [rdi + 2*rax], eax
  add rax, 2
  cmp rax, 200
  jne .LBB0_1
  ret
GCC与clang方法的优缺点是什么?i、 e.一个额外的变量单独递增,而不是通过更复杂的寻址模式相乘

注:

  • 这个问题也与大约相同的代码有关,但与
    float
    有关,而不是与
    int
    有关
    • 索引寻址(在加载和存储中,
      lea
      仍然不同)有一些折衷,例如

      • 在许多µarch上,使用索引寻址的指令比不使用索引寻址的指令的延迟稍长。但通常吞吐量是一个更重要的考虑因素
      • 在Netburst上,具有的存储将生成额外的µop,因此可能会降低吞吐量。SIB字节会导致额外的µop,无论您是否将其用于索引寻址,但索引寻址总是会花费额外的µop。它不适用于加载
      • 在Haswell/Broadwell(仍在Skylake/Kabylake中)上,具有索引寻址的存储不能用于地址生成,而是将使用一个更通用的地址生成端口,从而降低负载可用的吞吐量

      因此,对于加载,如果将add保存在某个位置,则使用索引寻址通常是好的(或不错的),除非它们是依赖加载链的一部分。对于存储,使用索引寻址更危险。在示例代码中,它不应该有太大的区别。保存
      add
      实际上并不相关,ALU指令不会成为瓶颈。端口2或端口3中发生的地址生成也不重要,因为没有负载。

      是的,如果索引未分解为比指针增量所需的额外UOP更多的UOP,请利用x86寻址模式的强大功能来节省UOP。

      (在许多情况下,展开并使用指针增量是一种胜利,因为英特尔Sandybridge系列上没有进行分层,但如果您不展开或仅使用
      mov
      加载,而不是将内存操作数折叠到ALU操作中进行微融合,则索引寻址模式在某些CPU上通常是收支平衡的,而在其他CPU上则是一种胜利。)

      如果你想在这里做出最佳选择,阅读和理解是非常重要的。(请注意,IACA的做法是错误的,它不会模拟Haswell,后来会保留一些UOP微熔合,因此你甚至不能让它为你做静态分析来检查你的工作。)


      索引寻址模式通常比较便宜。在最坏的情况下,它们会为前端()额外花费一个uop,并且/或者阻止存储地址uop使用port7(它只支持基本+置换寻址模式)。有关Intel在Haswell中添加的port7上的存储AGU的更多信息,请参阅并撰写相关文章。
      在Haswell+上,如果您需要循环在每个时钟上维持2次以上的内存操作,那么请避免索引存储

      充其量,除了机器代码编码中额外字节的代码大小成本之外,它们是免费的。(拥有索引寄存器需要编码中的SIB(缩放索引基)字节)

      通常情况下,唯一的惩罚是在Intel Sandybridge系列CPU上,与简单的
      [base+0-2047]
      寻址模式相比,额外增加1个加载使用延迟周期

      如果要在多条指令中使用索引寻址模式,通常只值得使用额外的指令来避免该寻址模式。(例如加载/修改/存储)


      如果您已经使用2寄存器寻址模式,则索引的缩放是免费的(至少在现代CPU上)。对于
      lea
      ,Agner Fog的表格列出了AMD Ryzen在
      lea
      具有缩放索引寻址模式(或3分量)时具有2c延迟和每时钟2吞吐量,否则为1c延迟和
      0.25c
      吞吐量。e、 g.
      lea-rax,[rcx+rdx]
      lea-rax,[rcx+2*rdx]
      快,但不值得使用额外的指令。)出于某种原因,Ryzen也不喜欢64位模式下的32位目标。但最坏情况下的LEA仍然不坏。无论如何,大多数情况下与加载的地址模式选择无关,因为大多数CPU(除了order Atom)在ALU上运行LEA,而不是用于实际加载/存储的AGU

      主要问题是一个未标度的寄存器(因此它可以是机器代码编码中的“基”寄存器:
      [base+idx*scale+disp]
      )或两个寄存器之间的问题。请注意,对于Intel的micro fusion限制,
      [disp32+idx*scale]
      (例如,索引静态阵列)是一种索引寻址模式


      这两个函数都不是完全最优的(即使不考虑展开或矢量化),但clang的看起来非常接近

      clang唯一能做得更好的事情就是通过使用
      addeax,2
      cmpeax,200
      避免REX前缀来节省2字节的代码大小。它将所有操作数提升到64位,因为它将它们与指针一起使用,我想这证明了C循环不需要它们包装,所以在asm中,它在任何地方都使用64位。这是毫无意义的;32位操作总是至少与64位操作一样快,并且隐式零扩展是免费的。但这只需要2字节的代码大小,并且除了间接的前端效果之外,不需要任何性能

      您已经构建了循环,因此编译器需要在寄存器中保留一个特定的值,并且不能完全将问题转化为指针增量+与结束指针的比较(当编译器除了数组索引之外不需要循环变量时,通常会这样做)

      您也不能将负索引的计数转换为零(编译器从来不会这样做,但会将循环开销减少到英特尔CPU上总共1个宏融合add+分支uop(它可以融合
      add+jcc
      ,而AMD只能融合test或cmp/jcc)。
      foo(int*): # @foo(int*)
        xor eax, eax
      .LBB0_1: # =>This Inner Loop Header: Depth=1
        mov dword ptr [rdi + 2*rax], eax
        add rax, 2
        cmp rax, 200
        jne .LBB0_1
        ret