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