使用GCC和GFORTRAN进行矢量化

时间:2018-07-02 11:51:39

标签: gcc compiler-optimization avx intel-fortran avx2

我有一个琐碎的循环,我期望在程序集中看到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

我也尝试过标志     -march =核心-avx2 -mtune =核心-avx2 对于gnu和intel,在gnu生产的程序集中,我仍然得到XMM相同的结果,但是在intel生产的程序集中却得到YMM

请大家做些不同的事情?

非常感谢, M

2 个答案:

答案 0 :(得分:2)

您忘记使用gfortran启用优化。使用gfortran -O3 -march=native

为此,请编写一个函数(子例程),该函数产生的结果是子例程可以看到的代码 outside 。例如以x作为参数并将其存储。编译器将必须发出可用于任何调用程序的asm,包括在调用该子例程后会关心数组内容的asm。


对于gcc,-ftree-vectorize仅在-O3上启用,而不是-O2

gcc的默认值为-O0,即快速编译并编写非常慢的代码以提供一致的调试。

gcc永远不会在-O0进行自动矢量化。您必须使用-O3-O2 -ftree-vectorize

与gcc不同,ifort默认值显然包括优化。如果您不对gcc使用ifort -S,则不应期望gcc -S-O3的输出会非常相似。


  

当我使用-O3时,它会丢弃程序集中对XMM和YMM的任何引用。

当编译器优化无用的工作时,这是一件好事。

编写一个接受数组输入arg并写入输出arg的函数,然后查看该函数的asm。或在两个全局数组上运行的函数。 不是整个程序,因为编译器具有整个程序的优化。

无论如何,请参见How to remove "noise" from GCC/clang assembly output?,以获取有关编写有用的函数以查看编译器asm输出的提示。这是C问题解答,但所有建议也适用于Fortran:编写带有args并返回结果或产生无法优化的副作用的函数。

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明智地优化了该函数,仅使用double从静态常量中复制了8 vmovupd个。增加阵列大小后,gcc会完全展开副本以提供惊人的大大小,最多143 double秒。但是对于144或更多,它将构成一个实际计算的循环。某处可能有一个调整参数来控制此启发式。顺便说一句,clang使用double甚至可以展开256个-O3 -march=haswell的副本。但是512足够大,以至于gcc和clang都可以进行计算的循环。

使用-O3 -march=haswell

gcc8.1的内部循环(带有-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位加载/存储。

Bulldozer和Zen在内部将256位指令拆分为两个128位,因此运行两倍多的XMM指令实际上可以更快,尤其是在您的数据不总是与32对齐的情况下。或者标量序言/结语开销是相关的。如果您使用的是AMD CPU,则绝对可以对这两种方法进行基准测试。或实际上,对于任何CPU来说,这都不是一个坏主意。

在这种情况下,gcc也没有意识到它应该使用整数的XMM向量和双精度的YMM向量。 (在适当的情况下,Clang和ICC可以更好地混合使用不同的矢量宽度)。而是每次都提取整数的YMM向量的高128。因此,有时会赢得128位向量化的一个原因是,当进行256位向量化时,gcc有时会shoot脚。(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覆盖它,例如如果您的数组通常是对齐的,但是您没有给编译器足够的信息。

答案 1 :(得分:1)

仅仅是整理一下,彼得斯的建议是正确的。我的代码现在看起来像:

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