使用 GCC 和 GFORTRAN 进行矢量化
Vectorization with GCC and GFORTRAN
我有一个简单的循环,我希望在程序集中看到 YMM 寄存器,但我只看到 XMM
program loopunroll
integer i
double precision x(8)
do i=1,8
x(i) = dble(i) + 5.0d0
enddo
end program loopunroll
然后我编译它(gcc或gfortran无所谓。我使用的是gcc 8.1.0)
[user@machine avx]$ gfortran -S -mavx loopunroll.f90
[user@machine avx]$ cat loopunroll.f90|grep mm
[user@machine avx]$ cat loopunroll.s|grep mm
vcvtsi2sd -4(%rbp), %xmm0, %xmm0
vmovsd .LC0(%rip), %xmm1
vaddsd %xmm1, %xmm0, %xmm0
vmovsd %xmm0, -80(%rbp,%rax,8)
但如果我这样做,intel parallel studio 2018 update3:
[user@machine avx]$ ifort -S -mavx loopunroll.f90
[user@machine avx]$ cat loopunroll.s|grep mm vmovdqu .L_2il0floatpacket.0(%rip), %xmm2 #11.8
vpaddd .L_2il0floatpacket.2(%rip), %xmm2, %xmm3 #11.15
vmovupd .L_2il0floatpacket.1(%rip), %ymm4 #11.23
vcvtdq2pd %xmm2, %ymm0 #11.15
vcvtdq2pd %xmm3, %ymm5 #11.15
vaddpd %ymm0, %ymm4, %ymm1 #11.8
vaddpd %ymm5, %ymm4, %ymm6 #11.8
vmovupd %ymm1, loopunroll_$X.0.1(%rip) #11.8
vmovupd %ymm6, 32+loopunroll_$X.0.1(%rip) #11.8
我也试过flags
-行军=核心-avx2 -mtune=核心-avx2
对于 gnu 和 intel,我仍然在 gnu 生产的程序集中得到相同的 XMM 结果,但在 intel 生产的程序集中得到 YMM
各位,我应该做些什么不同的事情?
非常感谢,
M
您忘记启用 gfortran
优化。使用 gfortran -O3 -march=native
.
为了不完全优化,编写一个函数(子例程)产生一个结果,该代码 在 子例程之外可以看到。例如以 x
作为参数并存储它。编译器必须发出适用于任何调用者的 asm,包括调用子例程后关心数组内容的调用者。
对于 gcc,-ftree-vectorize
仅在 -O3
启用,而不是 -O2
。
gcc 的默认值是 -O0
,即编译速度很快,但生成的代码非常慢,但调试一致。
gcc 永远不会在 -O0
处自动向量化。您必须使用 -O3
或 -O2 -ftree-vectorize
.
ifort
默认值显然包括优化,这与 gcc 不同。如果你不为 gcc 使用 -O3
,你不应该期望 ifort -S
和 gcc -S
输出远程相似。
when I use -O3 it throws away any reference to both XMM and YMM in the assembly.
编译器优化掉无用的工作是件好事。
编写一个接受数组输入 arg 并写入输出 arg 的函数,并查看该函数的 asm。或者在两个全局数组上运行的函数。 不是一个完整的程序,因为编译器有完整的程序优化。
无论如何,请参阅 以获取有关编写用于查看编译器 asm 输出的有用函数的提示。这是一个 C 问答,但所有建议也适用于 Fortran:编写带有 args 和 return 结果或具有无法优化的副作用的函数。
http://godbolt.org/ 没有 Fortran,看起来 -xfortran
无法使 g++
编译为 Fortran。 (不过,-xc
可以在 Godbolt 上编译为 C 而不是 C++。)否则我会推荐该工具来查看编译器输出。
我为您的循环制作了一个 C 版本,以查看 gcc 对其优化器可能类似的输入做了什么。 (我没有安装gfortran 8.1,对Fortran几乎一窍不通。我是来看AVX和优化标签的,但是gfortran使用的后端和我非常熟悉的gcc一样。)
void store_i5(double *x) {
for(int i=0 ; i<512; i++) {
x[i] = 5.0 + i;
}
}
以 i<8
作为循环条件,gcc -O3 -march=haswell
和 clang 明智地优化函数以从静态常量复制 8 double
s,vmovupd
。增加数组大小,gcc 完全展开一个副本,大小惊人,高达 143 double
s。但是对于 144 或更多,它会创建一个实际计算的循环。某处可能有一个调整参数来控制这种启发式。顺便说一句,即使是 256 double
秒,clang 也会完全展开一个副本,-O3 -march=haswell
。但是 512 足够大,gcc 和 clang 都可以循环计算。
gcc8.1 的内部循环(使用 -O3 -march=haswell
)看起来像这样,使用 -masm=intel
。 (参见 source+asm on the Godbolt compiler explorer)。
vmovdqa ymm1, YMMWORD PTR .LC0[rip] # [0,1,2,3,4,5,6,7]
vmovdqa ymm3, YMMWORD PTR .LC1[rip] # set1_epi32(8)
lea rax, [rdi+4096] # rax = endp
vmovapd ymm2, YMMWORD PTR .LC2[rip] # set1_pd(5.0)
.L2: # do {
vcvtdq2pd ymm0, xmm1 # packed convert 4 elements to double
vaddpd ymm0, ymm0, ymm2 # +5.0
add rdi, 64
vmovupd YMMWORD PTR [rdi-64], ymm0 # store x[i+0..3]
vextracti128 xmm0, ymm1, 0x1
vpaddd ymm1, ymm1, ymm3 # [i0, i1, i2, ..., i7] += 8 packed 32-bit integer add (d=dword)
vcvtdq2pd ymm0, xmm0 # convert the high 4 elements
vaddpd ymm0, ymm0, ymm2
vmovupd YMMWORD PTR [rdi-32], ymm0
cmp rax, rdi
jne .L2 # }while(p < endp);
我们可以通过使用偏移来阻止小型数组的常量传播,因此要存储的值不再是编译时常量:
void store_i5_var(double *x, int offset) {
for(int i=0 ; i<8; i++) {
x[i] = 5.0 + (i + offset);
}
}
gcc 使用与上面基本相同的循环体,有一些设置但相同的向量常量。
调整选项:
gcc -O3 -march=native
在某些目标上更喜欢使用 128 位向量进行自动向量化,因此您仍然不会获得 YMM 寄存器。您可以使用 -march=native -mprefer-vector-width=256
来覆盖它。 (https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html)。 (或者使用 gcc7 和更早版本,-mno-prefer-avx128`。)
gcc 比 -march=haswell
更喜欢 256 位,因为执行单元完全是 256 位,并且它具有高效的 256 位 loads/stores.
Bulldozer 和 Zen 在内部将 256 位指令拆分为两个 128 位指令,因此它实际上可以比 运行 两倍多的 XMM 指令更快,尤其是当您的数据并不总是按 32 位对齐时。或者当标量序言/结尾开销相关时。如果您使用的是 AMD CPU,则绝对要以两种方式进行基准测试。或者实际上对于任何 CPU 这不是一个坏主意。
同样在这种情况下,gcc 没有意识到它应该使用整数的 XMM 向量和双精度的 YMM 向量。 (Clang 和 ICC 在适当的时候更擅长混合不同的向量宽度)。相反,它每次都提取 YMM 整数向量的高 128 位。所以 128 位矢量化有时会获胜的一个原因是有时 gcc 在进行 256 位矢量化时搬起石头砸自己的脚。(gcc 的自动矢量化对于不支持的类型通常很笨拙宽度都一样。)
使用 -march=znver1 -mno-prefer-avx128
,gcc8.1 使用两个 128 位的一半存储到内存,因为它不知道目标是否是 32 字节对齐的 (https://godbolt.org/g/A66Egm) . tune=znver1
设置 -mavx256-split-unaligned-store
。您可以使用 -mno-avx256-split-unaligned-store
覆盖它,例如如果你的数组通常是对齐的,但你没有给编译器足够的信息。
整理一下,彼得的建议是正确的。我的代码现在看起来像:
program loopunroll
double precision x(512)
call looptest(x)
end program loopunroll
subroutine looptest(x)
integer i
double precision x(512)
do i=1,512
x(i) = dble(i) + 5.0d0
enddo
return
end subroutine looptest
并且生成 YMM 的方法是
gfortran -S -march=haswell -O3 loopunroll.f90
我有一个简单的循环,我希望在程序集中看到 YMM 寄存器,但我只看到 XMM
program loopunroll
integer i
double precision x(8)
do i=1,8
x(i) = dble(i) + 5.0d0
enddo
end program loopunroll
然后我编译它(gcc或gfortran无所谓。我使用的是gcc 8.1.0)
[user@machine avx]$ gfortran -S -mavx loopunroll.f90
[user@machine avx]$ cat loopunroll.f90|grep mm
[user@machine avx]$ cat loopunroll.s|grep mm
vcvtsi2sd -4(%rbp), %xmm0, %xmm0
vmovsd .LC0(%rip), %xmm1
vaddsd %xmm1, %xmm0, %xmm0
vmovsd %xmm0, -80(%rbp,%rax,8)
但如果我这样做,intel parallel studio 2018 update3:
[user@machine avx]$ ifort -S -mavx loopunroll.f90
[user@machine avx]$ cat loopunroll.s|grep mm vmovdqu .L_2il0floatpacket.0(%rip), %xmm2 #11.8
vpaddd .L_2il0floatpacket.2(%rip), %xmm2, %xmm3 #11.15
vmovupd .L_2il0floatpacket.1(%rip), %ymm4 #11.23
vcvtdq2pd %xmm2, %ymm0 #11.15
vcvtdq2pd %xmm3, %ymm5 #11.15
vaddpd %ymm0, %ymm4, %ymm1 #11.8
vaddpd %ymm5, %ymm4, %ymm6 #11.8
vmovupd %ymm1, loopunroll_$X.0.1(%rip) #11.8
vmovupd %ymm6, 32+loopunroll_$X.0.1(%rip) #11.8
我也试过flags -行军=核心-avx2 -mtune=核心-avx2 对于 gnu 和 intel,我仍然在 gnu 生产的程序集中得到相同的 XMM 结果,但在 intel 生产的程序集中得到 YMM
各位,我应该做些什么不同的事情?
非常感谢, M
您忘记启用 gfortran
优化。使用 gfortran -O3 -march=native
.
为了不完全优化,编写一个函数(子例程)产生一个结果,该代码 在 子例程之外可以看到。例如以 x
作为参数并存储它。编译器必须发出适用于任何调用者的 asm,包括调用子例程后关心数组内容的调用者。
对于 gcc,-ftree-vectorize
仅在 -O3
启用,而不是 -O2
。
gcc 的默认值是 -O0
,即编译速度很快,但生成的代码非常慢,但调试一致。
gcc 永远不会在 -O0
处自动向量化。您必须使用 -O3
或 -O2 -ftree-vectorize
.
ifort
默认值显然包括优化,这与 gcc 不同。如果你不为 gcc 使用 -O3
,你不应该期望 ifort -S
和 gcc -S
输出远程相似。
when I use -O3 it throws away any reference to both XMM and YMM in the assembly.
编译器优化掉无用的工作是件好事。
编写一个接受数组输入 arg 并写入输出 arg 的函数,并查看该函数的 asm。或者在两个全局数组上运行的函数。 不是一个完整的程序,因为编译器有完整的程序优化。
无论如何,请参阅
http://godbolt.org/ 没有 Fortran,看起来 -xfortran
无法使 g++
编译为 Fortran。 (不过,-xc
可以在 Godbolt 上编译为 C 而不是 C++。)否则我会推荐该工具来查看编译器输出。
我为您的循环制作了一个 C 版本,以查看 gcc 对其优化器可能类似的输入做了什么。 (我没有安装gfortran 8.1,对Fortran几乎一窍不通。我是来看AVX和优化标签的,但是gfortran使用的后端和我非常熟悉的gcc一样。)
void store_i5(double *x) {
for(int i=0 ; i<512; i++) {
x[i] = 5.0 + i;
}
}
以 i<8
作为循环条件,gcc -O3 -march=haswell
和 clang 明智地优化函数以从静态常量复制 8 double
s,vmovupd
。增加数组大小,gcc 完全展开一个副本,大小惊人,高达 143 double
s。但是对于 144 或更多,它会创建一个实际计算的循环。某处可能有一个调整参数来控制这种启发式。顺便说一句,即使是 256 double
秒,clang 也会完全展开一个副本,-O3 -march=haswell
。但是 512 足够大,gcc 和 clang 都可以循环计算。
gcc8.1 的内部循环(使用 -O3 -march=haswell
)看起来像这样,使用 -masm=intel
。 (参见 source+asm on the Godbolt compiler explorer)。
vmovdqa ymm1, YMMWORD PTR .LC0[rip] # [0,1,2,3,4,5,6,7]
vmovdqa ymm3, YMMWORD PTR .LC1[rip] # set1_epi32(8)
lea rax, [rdi+4096] # rax = endp
vmovapd ymm2, YMMWORD PTR .LC2[rip] # set1_pd(5.0)
.L2: # do {
vcvtdq2pd ymm0, xmm1 # packed convert 4 elements to double
vaddpd ymm0, ymm0, ymm2 # +5.0
add rdi, 64
vmovupd YMMWORD PTR [rdi-64], ymm0 # store x[i+0..3]
vextracti128 xmm0, ymm1, 0x1
vpaddd ymm1, ymm1, ymm3 # [i0, i1, i2, ..., i7] += 8 packed 32-bit integer add (d=dword)
vcvtdq2pd ymm0, xmm0 # convert the high 4 elements
vaddpd ymm0, ymm0, ymm2
vmovupd YMMWORD PTR [rdi-32], ymm0
cmp rax, rdi
jne .L2 # }while(p < endp);
我们可以通过使用偏移来阻止小型数组的常量传播,因此要存储的值不再是编译时常量:
void store_i5_var(double *x, int offset) {
for(int i=0 ; i<8; i++) {
x[i] = 5.0 + (i + offset);
}
}
gcc 使用与上面基本相同的循环体,有一些设置但相同的向量常量。
调整选项:
gcc -O3 -march=native
在某些目标上更喜欢使用 128 位向量进行自动向量化,因此您仍然不会获得 YMM 寄存器。您可以使用 -march=native -mprefer-vector-width=256
来覆盖它。 (https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html)。 (或者使用 gcc7 和更早版本,-mno-prefer-avx128`。)
gcc 比 -march=haswell
更喜欢 256 位,因为执行单元完全是 256 位,并且它具有高效的 256 位 loads/stores.
Bulldozer 和 Zen 在内部将 256 位指令拆分为两个 128 位指令,因此它实际上可以比 运行 两倍多的 XMM 指令更快,尤其是当您的数据并不总是按 32 位对齐时。或者当标量序言/结尾开销相关时。如果您使用的是 AMD CPU,则绝对要以两种方式进行基准测试。或者实际上对于任何 CPU 这不是一个坏主意。
同样在这种情况下,gcc 没有意识到它应该使用整数的 XMM 向量和双精度的 YMM 向量。 (Clang 和 ICC 在适当的时候更擅长混合不同的向量宽度)。相反,它每次都提取 YMM 整数向量的高 128 位。所以 128 位矢量化有时会获胜的一个原因是有时 gcc 在进行 256 位矢量化时搬起石头砸自己的脚。(gcc 的自动矢量化对于不支持的类型通常很笨拙宽度都一样。)
使用 -march=znver1 -mno-prefer-avx128
,gcc8.1 使用两个 128 位的一半存储到内存,因为它不知道目标是否是 32 字节对齐的 (https://godbolt.org/g/A66Egm) . tune=znver1
设置 -mavx256-split-unaligned-store
。您可以使用 -mno-avx256-split-unaligned-store
覆盖它,例如如果你的数组通常是对齐的,但你没有给编译器足够的信息。
整理一下,彼得的建议是正确的。我的代码现在看起来像:
program loopunroll
double precision x(512)
call looptest(x)
end program loopunroll
subroutine looptest(x)
integer i
double precision x(512)
do i=1,512
x(i) = dble(i) + 5.0d0
enddo
return
end subroutine looptest
并且生成 YMM 的方法是
gfortran -S -march=haswell -O3 loopunroll.f90