使用 GCC 和 GFORTRAN 进行矢量化

Posted

技术标签:

【中文标题】使用 GCC 和 GFORTRAN 进行矢量化【英文标题】:Vectorization with GCC and GFORTRAN 【发布时间】:2018-07-02 11:51:38 【问题描述】:

我有一个简单的循环,我希望在程序集中看到 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=core-avx2 -mtune=core-avx2 对于 gnu 和 intel,我仍然在 gnu 生产的程序集中得到相同的 XMM 结果,但在 intel 生产的程序集中得到 YMM

请问各位,我应该做些什么不同的事情?

非常感谢, M

【问题讨论】:

您忘记使用gfortran 启用优化。使用gfortran -O3 -march=native。 (对于 gcc,-ftree-vectorize 仅在 -O3 启用,-O2 不启用。默认为-O0,即编译速度快,代码非常慢,但调试始终如一。) 顺便说一句,请大家投票让xmm标签成为sse的同义词。 ***.com/tags/sse/synonyms. 嗨彼得,谢谢你的回复,但我试过了,但也失败了。事实上,当我使用 -O3 时,它会丢弃程序集中对 XMM 和 YMM 的任何引用。 所以写一个没有完全优化掉的函数!或者使用带有"memory" clobber 的内联汇编来停止优化器,如果这在 GNU fortran 中是可能的。有关编写用于查看编译器 asm 输出的有用函数的提示,请参阅 How to remove "noise" from GCC/clang assembly output?。 【参考方案1】:

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

为了不完全优化,编写一个函数(子例程),该函数(子例程)产生一个代码outside子例程可以看到的结果。例如将x 作为参数并存储它。编译器必须发出适用于任何调用者的 asm,包括在调用子例程后关心数组内容的调用者。


对于 gcc,-ftree-vectorize 仅在 -O3 启用,-O2 不启用。

gcc 的默认值为-O0,即编译速度快,生成的代码速度极慢,从而提供一致的调试。

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

ifort 默认显然包含优化,这与 gcc 不同。如果您不将 -O3 用于 gcc,则不应期望 ifort -Sgcc -S 输出远程相似。


当我使用 -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&lt;8 作为循环条件,gcc -O3 -march=haswell 和clang 明智地优化了函数,只从静态常量中复制8 个doubles,使用vmovupd。增加数组大小,gcc 会完全展开一个副本以获得惊人的大尺寸,最多 143 doubles。但是对于 144 或更多,它会产生一个实际计算的循环。某处可能有一个调整参数来控制这种启发式。顺便说一句,clang 完全展开副本,即使是 256 doubles,-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 位加载/存储。

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 覆盖它,例如如果您的数组通常是对齐的,但您没有向编译器提供足够的信息。

【讨论】:

嗨,彼得,我已经尝试过 -O3 -march=native -ftree-vectorize 并且还尝试过 -mtune 标志、-fpeel-loops、- mavx2,展开循环一个 dnone 工作。我也不想在这个阶段通过使用 -O2 或 -O3 来“把婴儿和洗澡水一起扔出去”。这里的重点是打破这个玩具代码,真正理解如何让 gcc 汇编器在组装 @Morph:用我建议的示例更新了我的答案。您必须使用 -O3 来获得自动矢量化,但您还必须更改您的源代码,使其不仅仅是 compile-time-constant 结果的一个小副本。 顺便说一句,感谢您抽出宝贵时间回复我的帖子并在需要的地方进行修饰(我是组装的完整新手)。 @Morph:好的,你机器上的-march=native-march=znver1。 gcc 的tune=znver1 可能包括-mprefer-avx128,因此您有时可能会看到只有XMM 向量的auto-vec。 (您可以使用-S -fverbose-asm 查看命令行启用/暗示的全套选项:顶部有一大块 cmets。) @Morph:使用 Fortran,您还在编写整个 程序 而不是子例程吗?如果是这样,编译器可以看到结果没有在整个程序中使用,并将其优化掉。请记住,您不需要它来链接或运行,您只需要编写一个编译为.o.s 的函数。或者要制作一个完整的程序,请将您的数组作为 arg 传递给另一个源文件中的函数。 (如果不进行链接时优化进行编译,则无法内联,因此编译器不能省略在内存中创建数组数据。)【参考方案2】:

只是为了整理一下,Peters 的建议是正确的。我的代码现在看起来像:

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

【讨论】:

以上是关于使用 GCC 和 GFORTRAN 进行矢量化的主要内容,如果未能解决你的问题,请参考以下文章

配置编译器(GCC和GFortran)

我可以为 gfortran 使用哪些 gcc 编译器选项

gfortran 编译器的强制向量化

Mac OS X 10.8.3 上的 gfortran/gcc4.8

在gfortran中使用“pragma GCC优化”

Linux下安装gcc g++ gfortran编译器