Performance 短指令与长指令
我目前正在编写一个编译器,即将实现代码生成。目前的目标指令集是x64。Performance 短指令与长指令,performance,x86-64,instruction-set,Performance,X86 64,Instruction Set,我目前正在编写一个编译器,即将实现代码生成。目前的目标指令集是x64。 现在x64是CISC,所以有许多复杂的指令。但我知道这些都是由CPU在内部转换成RISC的,之后还会出现无序执行。 因此,我的问题是:使用较短的指令(类似RISC)是否比使用较少的复杂指令对性能有影响?我的语言的测试程序没有那么大,所以我认为将指令放入缓存目前应该不是问题。不,使用大多数简单的x86指令(例如,避免push和使用sub-rsp,无论什么和使用mov存储args)对P5奔腾来说是一个有用的优化,因为它不知道如何
现在x64是CISC,所以有许多复杂的指令。但我知道这些都是由CPU在内部转换成RISC的,之后还会出现无序执行。
因此,我的问题是:使用较短的指令(类似RISC)是否比使用较少的复杂指令对性能有影响?我的语言的测试程序没有那么大,所以我认为将指令放入缓存目前应该不是问题。不,使用大多数简单的x86指令(例如,避免
push
和使用sub-rsp,无论什么
和使用mov
存储args)对P5奔腾来说是一个有用的优化,因为它不知道如何在内部拆分紧凑但复杂的指令。它的2宽超标量流水线只能对简单指令进行配对
现代x86 CPU(自Intel P6(奔腾pro/PIII)以来,包括所有x86-64 CPU)将复杂指令解码到多个UOP,这些UOP可以独立调度。(对于常见的复杂指令,如push
/pop
,它们有一些技巧将它们作为单个uop来处理。在这种情况下,堆栈引擎将堆栈指针重命名到内核无序部分之外,因此push
的rsp-=8
部分不需要uop)
内存源指令(如addeax、[rdi]
等)甚至可以通过将加载与ALU uop微融合,将其在无序调度程序中分离,以分配给执行单元,从而解码为英特尔CPU上的单个uop。在管道的其余部分,它只使用1个入口(在前端和ROB中)。(但请参阅带有索引寻址模式的Sandybridge的限制,在Haswell和更高版本上有所放松。)AMD CPU只是自然地将内存操作数与ALU指令融合,并且不用于将它们解码为额外的m-ops/uops,因此它没有一个奇特的名称
简单地说,指令长度并不是完全相关的。e、 g.idiv rcx
只有3个字节,但在Skylake上解码为57个uops。(避免64位除法,它比32位除法慢。)
代码越小越好,其他条件都一样。当足以避免REX前缀时,选择32位操作数大小,并选择不需要REX前缀的寄存器(如
ecx
而不是r8d
)。但通常不会花费额外的指令来实现这一点。(例如,使用r8d
而不是保存/恢复rbx
,因此您可以使用ebx
作为另一个暂存寄存器)
但当所有其他因素都不相同时,大小通常是高性能的最后优先事项,仅次于最小化UOP和缩短延迟依赖链(尤其是循环携带的依赖链)
- Agner Fog的优化指南和说明表:
- 英特尔的Sandy Bridge微体系结构,David Kanter深潜。 ()
大多数程序的大部分时间都花在足够小的循环中,以适应L1d缓存,而大部分时间则花在其中一些更小的循环中。 除非您能够正确识别“冷”代码(很少执行),否则使用3字节
push 1
/pop-rax
而不是5字节mov-eax之类的方法来优化大小,1
绝对不是一个好的默认值。clang/LLVM将使用-Oz
(仅针对大小进行优化),而不是-Os
(针对大小和速度的平衡进行优化)推送/弹出常量
使用inc
而不是add reg,1
保存一个字节(x86-64中只有1个字节,而32位代码中只有2个字节)。有了注册目的地,在大多数情况下在大多数CPU上的速度都一样快。看
现代主流x86 CPU已经解码了uop缓存(AMD从Ryzen开始,Intel从Sandybridge开始),这在很大程度上避免了平均指令长度>4的旧CPU上的前端瓶颈。 在此之前(Core2/Nehalem),避免前端瓶颈的调优要比平均使用简短指令复杂得多。请参阅Agner Fog的Microach指南,了解解码器在那些较旧的Intel CPU中可以处理的uop模式的详细信息,以及跳转后获取时相对于16字节边界的代码对齐的效果,等等 AMD推土机系列在L1i缓存中标记指令边界,如果集群的两个核心都处于活动状态,则每个周期最多可解码2倍16字节,否则Agner Fog的Microach PDF()报告每个周期约21字节(而Intel在不从uop缓存运行时,解码器每个周期最多可解码16字节)。推土机较低的后端吞吐量可能意味着前端瓶颈发生的频率较低。但我真的不知道,我还没有为推土机家族调整过任何可以使用硬件来测试任何东西的东西
示例:此函数使用叮当声与
-O3
、-Os
和-Oz
编译而成
int sum(int*arr) {
int sum = 0;
for(int i=0;i<10240;i++) {
sum+=arr[i];
}
return sum;
}
这是相当愚蠢的;它以8的速度展开,但仍然只有1个蓄能器。因此,自SnB和AMD自K8以来,它在英特尔的每个时钟吞吐量上的瓶颈是1个周期的延迟add
,而不是2个负载。(而且每个时钟周期只读取4个字节,可能不会对内存带宽造成太大的瓶颈。)
使用2个矢量累加器时,正常-O3(而不是禁用矢量化)效果更好:
sum: # @sum
pxor xmm0, xmm0 # zero first vector register
mov eax, 36
pxor xmm1, xmm1 # 2nd vector
.LBB2_1: # =>This Inner Loop Header: Depth=1
movdqu xmm2, xmmword ptr [rdi + 4*rax - 144]
paddd xmm2, xmm0
movdqu xmm0, xmmword ptr [rdi + 4*rax - 128]
paddd xmm0, xmm1
movdqu xmm1, xmmword ptr [rdi + 4*rax - 112]
movdqu xmm3, xmmword ptr [rdi + 4*rax - 96]
movdqu xmm4, xmmword ptr [rdi + 4*rax - 80]
paddd xmm4, xmm1
paddd xmm4, xmm2
movdqu xmm2, xmmword ptr [rdi + 4*rax - 64]
paddd xmm2, xmm3
paddd xmm2, xmm0
movdqu xmm1, xmmword ptr [rdi + 4*rax - 48]
movdqu xmm3, xmmword ptr [rdi + 4*rax - 32]
movdqu xmm0, xmmword ptr [rdi + 4*rax - 16]
paddd xmm0, xmm1
paddd xmm0, xmm4
movdqu xmm1, xmmword ptr [rdi + 4*rax]
paddd xmm1, xmm3
paddd xmm1, xmm2
add rax, 40
cmp rax, 10276
jne .LBB2_1
paddd xmm1, xmm0 # add the two accumulators
# and horizontal sum the result
pshufd xmm0, xmm1, 78 # xmm0 = xmm1[2,3,0,1]
paddd xmm0, xmm1
pshufd xmm1, xmm0, 229 # xmm1 = xmm0[1,1,2,3]
paddd xmm1, xmm0
movd eax, xmm1 # extract the result into a scalar integer reg
ret
这个版本的展开可能比它需要的要多;循环开销很小,movdqu
+padd
只有2个uops,因此我们在前端远远不是瓶颈。如果每个时钟加载2个movdqu
loads,假设数据在L1d缓存或L2缓存中是热的,则该循环可以在每个时钟周期处理32字节的输入,否则它将运行得较慢。这超过最小展开量将放出o
sum: # @sum
pxor xmm0, xmm0 # zero first vector register
mov eax, 36
pxor xmm1, xmm1 # 2nd vector
.LBB2_1: # =>This Inner Loop Header: Depth=1
movdqu xmm2, xmmword ptr [rdi + 4*rax - 144]
paddd xmm2, xmm0
movdqu xmm0, xmmword ptr [rdi + 4*rax - 128]
paddd xmm0, xmm1
movdqu xmm1, xmmword ptr [rdi + 4*rax - 112]
movdqu xmm3, xmmword ptr [rdi + 4*rax - 96]
movdqu xmm4, xmmword ptr [rdi + 4*rax - 80]
paddd xmm4, xmm1
paddd xmm4, xmm2
movdqu xmm2, xmmword ptr [rdi + 4*rax - 64]
paddd xmm2, xmm3
paddd xmm2, xmm0
movdqu xmm1, xmmword ptr [rdi + 4*rax - 48]
movdqu xmm3, xmmword ptr [rdi + 4*rax - 32]
movdqu xmm0, xmmword ptr [rdi + 4*rax - 16]
paddd xmm0, xmm1
paddd xmm0, xmm4
movdqu xmm1, xmmword ptr [rdi + 4*rax]
paddd xmm1, xmm3
paddd xmm1, xmm2
add rax, 40
cmp rax, 10276
jne .LBB2_1
paddd xmm1, xmm0 # add the two accumulators
# and horizontal sum the result
pshufd xmm0, xmm1, 78 # xmm0 = xmm1[2,3,0,1]
paddd xmm0, xmm1
pshufd xmm1, xmm0, 229 # xmm1 = xmm0[1,1,2,3]
paddd xmm1, xmm0
movd eax, xmm1 # extract the result into a scalar integer reg
ret
# clang -Os
.LBB2_1: # =>This Inner Loop Header: Depth=1
movdqu xmm1, xmmword ptr [rdi + 4*rax]
paddd xmm0, xmm1
add rax, 4
cmp rax, 10240
jne .LBB2_1
# clang -Os -fno-vectorize
sum: # @sum
xor ecx, ecx
xor eax, eax
.LBB2_1: # =>This Inner Loop Header: Depth=1
add eax, dword ptr [rdi + 4*rcx]
inc rcx
cmp rcx, 10240
jne .LBB2_1
ret
sum: # @sum
xor ecx, ecx
xor eax, eax
jmp .LBB2_1
.LBB2_3: # in Loop: Header=BB2_1 Depth=1
add eax, dword ptr [rdi + 4*rcx]
inc rcx
.LBB2_1: # =>This Inner Loop Header: Depth=1
cmp rcx, 10240
jne .LBB2_3
ret