C 优化简单的模具操作,将变量保留在寄存器中
我试图使下面的代码更快地将两个变量(我们需要重用的变量)保留在寄存器中或比缓存更近的任何位置。该代码将数组中位于C 优化简单的模具操作,将变量保留在寄存器中,c,optimization,x86,cpu-registers,cpu-cache,C,Optimization,X86,Cpu Registers,Cpu Cache,我试图使下面的代码更快地将两个变量(我们需要重用的变量)保留在寄存器中或比缓存更近的任何位置。该代码将数组中位于idx位置的三个相邻元素相加 void stencil(double * input, double * output){ unsigned int idx = 1; output[0] = input[0] + input[1]; for(; idx < SIZE - 1; idx++){ output[idx] = input[id
idx
位置的三个相邻元素相加
void stencil(double * input, double * output){
unsigned int idx = 1;
output[0] = input[0] + input[1];
for(; idx < SIZE - 1; idx++){
output[idx] = input[idx-1] + input[idx] + input[idx+1];
}
output[idx] = input[idx-1] + input[idx];
}
使用-march=native
标志时,如下所示:
stencil:
.LFB20:
.cfi_startproc
vmovsd (%rdi), %xmm1
vxorpd %xmm0, %xmm0, %xmm0
leaq 144(%rdi), %rdx
leaq 136(%rsi), %rax
xorl %ecx, %ecx
.p2align 4,,10
.p2align 3
.L2:
vaddsd %xmm1, %xmm0, %xmm0
vmovsd -136(%rdx), %xmm4
prefetcht0 (%rdx)
addl $8, %ecx
prefetchw (%rax)
addq $64, %rdx
addq $64, %rax
vaddsd %xmm1, %xmm4, %xmm1
vaddsd %xmm4, %xmm0, %xmm0
vmovsd %xmm0, -200(%rax)
vmovsd -192(%rdx), %xmm3
vaddsd %xmm3, %xmm1, %xmm1
vaddsd %xmm3, %xmm4, %xmm4
vmovsd %xmm1, -192(%rax)
vmovsd -184(%rdx), %xmm2
vaddsd %xmm2, %xmm4, %xmm4
vaddsd %xmm2, %xmm3, %xmm3
vmovsd %xmm4, -184(%rax)
vmovsd %xmm4, -184(%rax)
vmovsd -176(%rdx), %xmm0
vaddsd %xmm0, %xmm3, %xmm3
vaddsd %xmm0, %xmm2, %xmm2
vmovsd %xmm3, -176(%rax)
vmovsd -168(%rdx), %xmm1
vaddsd %xmm1, %xmm2, %xmm2
vaddsd %xmm1, %xmm0, %xmm0
vmovsd %xmm2, -168(%rax)
vmovsd -160(%rdx), %xmm2
vaddsd %xmm2, %xmm0, %xmm0
vaddsd %xmm2, %xmm1, %xmm1
vmovsd %xmm0, -160(%rax)
vmovsd -152(%rdx), %xmm0
vaddsd %xmm0, %xmm1, %xmm1
vaddsd %xmm0, %xmm2, %xmm2
vmovsd %xmm1, -152(%rax)
vmovsd -144(%rdx), %xmm1
vaddsd %xmm1, %xmm2, %xmm2
vmovsd %xmm2, -144(%rax)
cmpl $1399999992, %ecx
jne .L2
movabsq $11199999944, %rdx
movabsq $11199999936, %rcx
addq %rdi, %rdx
addq %rsi, %rcx
xorl %eax, %eax
jmp .L3
.p2align 4,,7
.p2align 3
.L4:
vmovaps %xmm2, %xmm1
.L3:
vaddsd %xmm0, %xmm1, %xmm0
vmovsd (%rdx,%rax), %xmm2
vaddsd %xmm2, %xmm0, %xmm0
vmovsd %xmm0, (%rcx,%rax)
addq $8, %rax
vmovaps %xmm1, %xmm0
cmpq $56, %rax
jne .L4
vaddsd %xmm2, %xmm1, %xmm1
movabsq $11199999992, %rax
vmovsd %xmm1, (%rsi,%rax)
ret
有人对如何让GCC将变量保存到寄存器中以加快代码速度有什么建议吗?或者通过任何其他方式使代码绕过缓存而有效?使用寄存器旋转时,通常最好展开循环。除非明确要求,否则gcc不会这样做 下面是一个4级循环展开的示例
void stencil(double * input, double * output){
double x, y, z, w, u, v ;
x=0.0;
y=input[0];
int idx=0;
for(; idx < SIZE - 5; idx+=4){
z=input[idx+1];
w=input[idx+2];
u=input[idx+3];
v=input[idx+4];
output[idx] =x+y+z;
output[idx+1]=y+z+w;
output[idx+2]=z+w+u;
output[idx+3]=w+u+v;
x=u;
y=v;
}
z=input[idx+1];
w=input[idx+2];
u=input[idx+3];
output[idx] =x+y+z;
output[idx+1]=y+z+w;
output[idx+2]=z+w+u;
output[idx+3]=w+u;
}
void模具(双*输入,双*输出){
双x,y,z,w,u,v;
x=0.0;
y=输入[0];
int-idx=0;
对于(;idx
通过idx值有一个内存读写,每两个idx值有一个寄存器拷贝
可以尝试不同的展开级别,但每次迭代总是有2个寄存器副本,4个似乎是一个很好的折衷方案
如果大小不是4的倍数,则需要开场白
void stencil(double * input, double * output){
double x, y, z, w, u, v ;
int idx=0;
int remain=SIZE%4;
x=0.0;y=input[0]
switch (remain) {
case 3: z=input[++idx]; output[idx-1]=x+y+z; x=y; y=z;
case 2: z=input[++idx]; output[idx-1]=x+y+z; x=y; y=z;
case 1: z=input[++idx]; output[idx-1]=x+y+z; x=y; y=z;
}
for(; idx < SIZE - 5; idx+=4){
z=input[idx+1];
....
void模具(双*输入,双*输出){
双x,y,z,w,u,v;
int-idx=0;
int剩余=大小%4;
x=0.0;y=输入[0]
开关(保持){
情况3:z=输入[++idx];输出[idx-1]=x+y+z;x=y;y=z;
情况2:z=输入[++idx];输出[idx-1]=x+y+z;x=y;y=z;
情况1:z=输入[++idx];输出[idx-1]=x+y+z;x=y;y=z;
}
对于(;idx
正如预期的那样,asm相当复杂,很难说会有什么好处
您也可以尝试在原始代码上使用
-funroll循环。
编译器非常好,可能会提供更好的解决方案。这是一个好主意,但如果编译器知道这是安全的,他们已经可以为您这样做了。使用双*限制输出
和常量双*限制输入
来保证存储到输出[]
的编译器不会更改从输入[]
读取的内容
但使用SIMD的自动矢量化是一个更重要的优化,每个指令产生2或4个双结果。在检查重叠后,GCC和ICC将在-O3
处执行此操作。(但clang无法自动矢量化,只需使用标量[v]展开即可。)addsd
避免不必要的重新加载
不幸的是,您的优化版本无法实现自动矢量化!(这是编译器的错误,即当它知道输出没有重叠时,错过了优化错误,因此从内存中重新读取源代码与否是等价的)
看起来gcc在原始版本上做得很好,带有-O3-march=native
(特别是在为英特尔调优时,使用AVX的更宽向量是值得的。)我并行计算了3个未对齐加载和2个vaddpd ymm的4个双
结果
它在使用矢量化循环之前检查重叠。您可以使用double*restrict output
和input
来保证指针不重叠,因此不需要回退循环
L1d缓存带宽在现代CPU上非常出色;重新加载相同的数据不是什么大问题(每个时钟加载2次)。指令吞吐量更大。内存源addsd
的成本并不比将数据保存在寄存器中的成本高多少
如果使用128位向量进行矢量化,则有必要保留[idx+1..2]
中的向量,以便在下一次迭代中用作[idx+-1..1]
向量。GCC实际上就是这样做的
但是,当您为每条指令生成4个结果时,一次迭代的3个输入向量中没有一个对下一次迭代直接有用。不过,通过无序排列来节省一些加载端口带宽,以便从加载结果创建3个向量中的一个可能会有用。如果我使用\uuum256d
内部函数手动矢量化,我会尝试这样做。或使用128位向量的浮点
不幸的是,gcc正在使用索引寻址模式,因此带有内存源的vaddpd
指令将SnB系列前端(包括您的Broadwell Xeon E5-2698 v4)反分层为2个UOP
吞吐量分析,请参阅和
GCC的循环是8个融合域UOP,用于前端发布/重命名阶段发送到无序后端。这意味着前端最大吞吐量为每2个周期迭代1次
Skylake之前的Intel中的[v]addpd
只能在端口1上运行,而[v]mulpd
或FMA的吞吐量是前者的两倍。(Skylake放弃了专用的FP add单元,并以与mul和FMA相同的方式运行FP add。)因此,这也是每个迭代瓶颈2个周期
我们有3个加载+1个存储,所有这些加载都需要端口2或3中的一个(索引寻址模式存储不能使用端口7上的专用存储AGU)。因此,每个迭代瓶颈还有2个周期。但不是真的;跨越缓存线边界的未对齐加载更昂贵。实验表明Intel Skylake(可能还有Broadwell)重放被发现是缓存线拆分的加载UOP,因此它们再次运行以从第二个缓存线获取数据
我们的数据是8字节对齐的,但我们在所有8字节偏移量上均匀分布32字节负载
void stencil(double * input, double * output){
double x, y, z, w, u, v ;
x=0.0;
y=input[0];
int idx=0;
for(; idx < SIZE - 5; idx+=4){
z=input[idx+1];
w=input[idx+2];
u=input[idx+3];
v=input[idx+4];
output[idx] =x+y+z;
output[idx+1]=y+z+w;
output[idx+2]=z+w+u;
output[idx+3]=w+u+v;
x=u;
y=v;
}
z=input[idx+1];
w=input[idx+2];
u=input[idx+3];
output[idx] =x+y+z;
output[idx+1]=y+z+w;
output[idx+2]=z+w+u;
output[idx+3]=w+u;
}
void stencil(double * input, double * output){
double x, y, z, w, u, v ;
int idx=0;
int remain=SIZE%4;
x=0.0;y=input[0]
switch (remain) {
case 3: z=input[++idx]; output[idx-1]=x+y+z; x=y; y=z;
case 2: z=input[++idx]; output[idx-1]=x+y+z; x=y; y=z;
case 1: z=input[++idx]; output[idx-1]=x+y+z; x=y; y=z;
}
for(; idx < SIZE - 5; idx+=4){
z=input[idx+1];
....
#define SIZE 1000000
void stencil_restrict(double *restrict input, double *restrict output)
{
int idx = 1;
output[0] = input[0] + input[1];
for(; idx < SIZE - 1; idx++){
output[idx] = input[idx-1] + input[idx] + input[idx+1];
}
output[idx] = input[idx-1] + input[idx];
}
stencil_restrict:
vmovsd xmm0, QWORD PTR [rdi]
vaddsd xmm0, xmm0, QWORD PTR [rdi+8]
xor eax, eax
vmovsd QWORD PTR [rsi], xmm0 # first iteration
### Main loop
.L12:
vmovupd ymm2, YMMWORD PTR [rdi+8+rax] # idx +0 .. +3
vaddpd ymm0, ymm2, YMMWORD PTR [rdi+rax] # idx -1 .. +2
vaddpd ymm0, ymm0, YMMWORD PTR [rdi+16+rax] # idx +1 .. +4
vmovupd YMMWORD PTR [rsi+8+rax], ymm0 # store idx +0 .. +3
add rax, 32 # byte offset += 32
cmp rax, 7999968
jne .L12
# cleanup of last few elements
vmovsd xmm1, QWORD PTR [rdi+7999976]
vaddsd xmm0, xmm1, QWORD PTR [rdi+7999968]
vaddsd xmm1, xmm1, QWORD PTR [rdi+7999984]
vunpcklpd xmm0, xmm0, xmm1
vaddpd xmm0, xmm0, XMMWORD PTR [rdi+7999984]
vmovups XMMWORD PTR [rsi+7999976], xmm0
vmovsd xmm0, QWORD PTR [rdi+7999984]
vaddsd xmm0, xmm0, QWORD PTR [rdi+7999992]
vmovsd QWORD PTR [rsi+7999992], xmm0
vzeroupper
ret
vmovupd ymm2, YMMWORD PTR [rdi+8+rax] # 1 uop, no micro-fusion
vaddpd ymm0, ymm2, YMMWORD PTR [rdi+rax] # 2 uops. (micro-fused in decoders/uop cache, unlaminates)
vaddpd ymm0, ymm0, YMMWORD PTR [rdi+16+rax] # 2 uops. (ditto)
vmovupd YMMWORD PTR [rsi+8+rax], ymm0 # 1 uop (stays micro-fused, but can't use the port 7 store AGU)
add rax, 32 # 1 uop
cmp rax, 7999968 # 0 uops, macro-fuses with JNE
jne .L12 # 1 uop
# prologue to reach an alignment boundary somewhere?
.L12:
movupd xmm2, XMMWORD PTR [rdi+rax]
movupd xmm1, XMMWORD PTR [rdi+8+rax]
addpd xmm0, xmm2
addpd xmm0, xmm1
movups XMMWORD PTR [rsi+rax], xmm0
add rax, 16
movapd xmm0, xmm1 # x = z
cmp rax, 7999992
jne .L12
# prologue to reach an alignment boundary so one load can be aligned.
# r10=input and r9=input+8 or something like that
# r8=output
.L18: # do {
movupd xmm0, XMMWORD PTR [r10+rdx]
add ecx, 1
addpd xmm0, xmm1 # x+y
movapd xmm1, XMMWORD PTR [r9+rdx] # z for this iteration, x for next
addpd xmm0, xmm1 # (x+y) + z
movups XMMWORD PTR [r8+rdx], xmm0
add rdx, 16
cmp ecx, r11d
jb .L18 # } while(i < max);