如何指示 gfortran 仅使用可分配数组的矢量化(最高可用 SIMD)指令来编译循环?

ove*_*yan 5 fortran simd gfortran avx2 intel-fortran

当 gfortran 对方程(例如 x = y*z )进行向量化时,编译器将仅使用向量寄存器(例如 YMM)和向量操作码,当且仅当编译器知道所有数据 (x,y,z) 都可以是加载到所述向量寄存器上。然而,如果编译器不知道是否所有数据都可以加载到寄存器中,它将生成冗余代码,以便可以操作剩余的数据。

此冗余代码通常不会使用最高可用的 SIMD 向量寄存器,并且会使可执行文件膨胀。我还没有进行广泛的测试,但常见的推理使我相信它可能会导致 ICACHE 缺失或阻止函数/子例程内联优化。

我想删除这个冗余代码,因为我知道我的所有数据 (x,y,z) 将完美地适合 YMM 向量寄存器。

我之前曾在这里更详细地描述过我的观察结果:https: //www.cfd-online.com/Forums/main/231759-fortran- assembly-how-remove-redundant-non-vectorized-code.html

不过,我想在这里举一些简单的例子来展示我的观点。

对于以下操作:x(:) = y*z其中x,y,z都是可分配数组,编译器不知道该操作是否可以完全矢量化,因为它不知道数组的长度是多少。如果 x,y,z 是整数数组并且长度是 8 的倍数,我们可以推断整个操作可以简单地使用 AVX2 向量化操作来完成。然而,由于数组长度在编译时未知,编译器必须生成可用于任意长度数组的冗余代码。

上帝螺栓链接:https://gcc.godbolt.org/z/1fdzK8

这是为该x(:) = y*z部分生成的程序集:

.L7:
        vmovdqu ymm1, YMMWORD PTR [r13+0+rax]
        vpmulld ymm0, ymm1, YMMWORD PTR [r12+rax]
        vmovdqu YMMWORD PTR [r14+rax], ymm0
        add     rax, 32
        cmp     rdx, rax
        jne     .L7
        mov     rdx, r15
        and     rdx, -8
        lea     rax, [rdx+1]
        cmp     rdx, r15
        je      .L22
        vzeroupper
.L6:
        lea     rdx, [rax-1]
        mov     esi, DWORD PTR [r13+0+rdx*4]
        imul    esi, DWORD PTR [r12+rdx*4]
        mov     DWORD PTR [r14+rdx*4], esi
        lea     rdx, [rax+1]
        cmp     r15, rdx
        jl      .L10
        mov     esi, DWORD PTR [r13+0+rax*4]
        imul    esi, DWORD PTR [r12+rax*4]
        mov     DWORD PTR [r14+rax*4], esi
        lea     rsi, [rax+2]
        cmp     r15, rsi
        jl      .L10
        mov     edi, DWORD PTR [r13+0+rdx*4]
        imul    edi, DWORD PTR [r12+rdx*4]
        mov     DWORD PTR [r14+rdx*4], edi
        lea     rdx, [rax+3]
        cmp     r15, rdx
        jl      .L10
        mov     edi, DWORD PTR [r13+0+rsi*4]
        imul    edi, DWORD PTR [r12+rsi*4]
        mov     DWORD PTR [r14+rsi*4], edi
        lea     rsi, [rax+4]
        cmp     r15, rsi
        jl      .L10
        mov     edi, DWORD PTR [r13+0+rdx*4]
        imul    edi, DWORD PTR [r12+rdx*4]
        mov     DWORD PTR [r14+rdx*4], edi
        lea     rdx, [rax+5]
        cmp     r15, rdx
        jl      .L10
        mov     edi, DWORD PTR [r13+0+rsi*4]
        add     rax, 6
        imul    edi, DWORD PTR [r12+rsi*4]
        mov     DWORD PTR [r14+rsi*4], edi
        cmp     r15, rax
        jl      .L10
        mov     eax, DWORD PTR [r12+rdx*4]
        imul    eax, DWORD PTR [r13+0+rdx*4]
        mov     DWORD PTR [r14+rdx*4], eax
Run Code Online (Sandbox Code Playgroud)

我们可以清楚地看到上面代码的.L7部分包含矢量化代码,.L6部分包含冗余代码,以确保任意长度的数组都可以使用相同的代码。

虽然这是一个很好的功能,但我想可靠地关闭它并指示编译器仅在 .L7 部分生成矢量化代码。我还没有进行广泛的性能测试,但是我仍然想删除 .L6 部分。

我能够使英特尔的 Fortran 编译器仅使用编译器指令生成矢量化代码部分,例如:

!DIR$ ASSUME_ALIGNED X: 32
!DIR$ ASSUME (MOD(N,8) .EQ. 0)
do i=1,n
    x(i) = y(i)*z(i)
enddo
Run Code Online (Sandbox Code Playgroud)

上帝螺栓链接:https://gcc.godbolt.org/z/PPf1zE

生成的程序集是:

..B1.15:                        # Preds ..B1.15 ..B1.14
        vmovdqu   ymm0, YMMWORD PTR [r13+r12*4]                 #22.12
        vpmulld   ymm1, ymm0, YMMWORD PTR [r14+r12*4]           #22.5
        vmovdqu   YMMWORD PTR [rax+r12*4], ymm1                 #22.5
        add       r12, 8                                        #21.1
        cmp       r12, rcx                                      #21.1
        jb        ..B1.15       # Prob 82%                      #21.1
Run Code Online (Sandbox Code Playgroud)

我还能够通过使用一些编译器巫术使 gfortran 编译器仅生成矢量化代码部分。我们只是用来do i=1,AND(N,-8)指示编译器循环以 8 的倍数结束。从数学上讲,它发生的原因是AND(N,-8) == N - MOD(N,8)

do i=1,AND(n,-8)
    x(i) = y(i)*z(i)
end do
Run Code Online (Sandbox Code Playgroud)

上帝螺栓链接: https: //gcc.godbolt.org/z/MW7cPG

生成的程序集是:

.L19:
        mov     rcx, QWORD PTR [rsp+16]
        vmovdqu ymm4, YMMWORD PTR [rcx+rax]
        mov     rcx, QWORD PTR [rsp+8]
        vpmulld ymm0, ymm4, YMMWORD PTR [r15+rax]
        vmovdqu YMMWORD PTR [rcx+rax], ymm0
        add     rax, 32
        cmp     rax, rdx
        jne     .L19
Run Code Online (Sandbox Code Playgroud)

然而,这对于 gfortran 来说并不是一个可靠的方法,并且无法消除我的一些测试用例中的冗余膨胀。AFAIK gfortran 没有任何像 这样的编译器指令!DIR$ ASSUME (MOD(N,8) .EQ. 0),所以这个方法对我来说似乎非常不可靠。

我希望以可靠且一致的方式进行此优化。

额外数据:

对看似复杂的代码执行了相同的操作:

vols(:) = 0.5*sqrt((x1*(y2-y3))**2.0 + (x2*(y3-y1))**2.0 + (x3*(y1-y2))**2.0)
Run Code Online (Sandbox Code Playgroud)

上帝螺栓链接: https: //gcc.godbolt.org/z/oxcWzd

在上面的示例中,生成的膨胀相当高。

上述优化适用于 Intel 的 Fortran 编译器。

上帝螺栓链接:https://gcc.godbolt.org/z/Y5orh1

上述优化适用于 gfortran 编译器。

上帝螺栓链接:https://gcc.godbolt.org/z/1or59d

Fed*_*ini 2

从哲学上来说,Fortran 不希望用户修改低级优化,因为该语言应该能够实现这一点。在您的情况下,可以通过将体积定义为函数来实现完全矢量化elemental

  elemental real function triVolume(x1,x2,x3,y1,y2,y3) result(vol)
     real, intent(in) :: x1,x2,x3,y1,y2,y3
     vol = 0.5*sqrt((x1*(y2-y3))**2 + (x2*(y3-y1))**2 + (x3*(y1-y2))**2)
  end function triVolume
Run Code Online (Sandbox Code Playgroud)

forall然后使用or循环它do concurrent

! Calculate volume of a triangle using cross product
do concurrent (i=1:n)
    vols(i) = triVolume(x1(i),x2(i),x3(i),y1(i),y2(i),y3(i))
end do
Run Code Online (Sandbox Code Playgroud)

这会产生干净的矢量化代码

请注意,do concurrent此处启用了 CPU 矢量化,但在未来的某些版本中,可能会隐藏有趣的内容,例如卸载到 GPU。

  • 对于基本函数,您可以跳过“do”循环并仅声明“vols = triVolume(x1,x2,x3,y1,y2,y3)” (2认同)