Fortran 数组数据、参数和矢量化

面向英特尔® MIC 架构的编译器方法

矢量化要素、Fortran 数组数据、参数和矢量化

概述

本文举例说明了 Fortran 中不同的数组类型及其作为局部变量、函数/子例程参数的应用并且提供了有关 Fortran 指针及其使用情况的示例。本文介绍了编译器如何对不同的数组数据类型和参数进行矢量化处理。其中包括关于高级源代码的示例,并详细解释了编译器针对这些情况所生成的代码。

主题

Fortran 数组数据、参数和矢量化示例

现代化的 Fortran 语言具有不同类型的数组并且提供了数组分割功能以便使数组的分段可作为参数传输至函数或者由 Fortran 指针来指向。对于在数组上运行的循环来说,编译器可以生成单位步长(unit stride)矢量代码、非单位步长代码或多版本代码,这会造成不能及时确认针对运行时执行的代码版本。编译器根据编程人员所选的数组类型为循环创建代码,无论是否针对该数组使用数组分割,或者数组是否作为函数参数进行传输。

对于英特尔® 至强 融核协处理器具有非单位步长内存访问的代码使用收集/分散指令 (vgather/vscatter)具有单位步长内存访问的代码可以使用更加高效的加载和存储。具有单位步长内存访问的代码可以使用对齐的加载/存储指令(vmovaps 等),或基于目标内存位置地址对齐的非对齐的加载/存储指令 (vloadunpack/vpackstore)

对于编译器生成单位步长矢量化代码的情况数组必须对齐才能实现较高的性能即使用对齐的加载/存储指令。编程人员可使用编译器选项 (-align array64byte) 或指令 (!dir$ attribute align) 命令编译器为数组分配对齐的内存。但是,这还不足以为循环生成对齐的矢量代码。此外,编程人员还应使用指令 (!dir$ vector aligned, !dir$ assume_aligned) 告诉每个循环的编译器可通过对齐的方式访问哪些数组。本指南包含了与“对齐优化”章节中介绍的对齐有关的详细信息。

对于具有非对齐数组的循环编译器通常会生成剥离循环peel loop)”。剥离循环先于主循环在数组的数据元素上运行,直到其中一个数组对齐(矢量化剥离循环使用收集/分散指令)。在剥离循环之后,针对该数组使用对齐内存访问的“内核循环”在对齐数据上运行。在这两个循环之后,剩余循环(remainder loop)将在数组结尾处(内核循环中断的地方)的剩余的非对齐数据上运行,直到所有迭代全部完成(矢量化剩余循环使用收集/分散指令)。对于内核循环,编译器可以针对第二个数组以及多版本循环的对齐生成一个动态检查(一个循环使用该数组的对齐访问,另外一个使用非对齐访问)。

1: 显形数组

对于参数是显形数组的子例程来说编译器在子例程/函数假设这些参数的数据是连续的中生成代码。
调用过程:编译器针对该子例程的调用而动态生成的代码可确保数组数据传输的连续性。如果实际参数确实是连续的或者是一个连续的数组段,那么一个指向实际数组或连续数组段的指针将会作为参数传输。如果数据是非连续的,例如非连续数据的数组段,调用程序将生成一个临时数组、将非连续源数据收集到该连续的临时数组中并为连续临时数据分配一个指针。实际的子例程/函数调用使用临时数组作为参数。调用返回时,调用代码将临时数据解压到原始的非连续源数组段并破坏临时数组。这会导致临时数组开销,以及执行收集/分散操作所需的内存移动的开销。

案例 1.1作为参数传输的显形数组。

subroutine explicit1(A, B, C)

  real, intent(in),    dimension(400,500) :: A

  real, intent(out),   dimension(500)     :: B

  real, intent(inout), dimension (400)    :: C

  integer i

 

!..loop 1

  do i=1,500

    B(i) = A(3,i)

  end do

 

!...loop 2

  do i=1,400

   C(i) = C(i) + A(i, 400)

  end do

end

Loop 1:

·         源代码在 B 上具有单位步长访问 A 上具有非单位步长访问。

·         循环被剥离,直到 B 对齐。

·         剥离循环进行矢量化处理但使用 vgather 来加载 A非对齐),使用 vscatter 来存储 B非对齐

·         内核循环使用 vmovaps 来存储 B对齐),使用 vgather 来加载 A对齐

·         剩余循环进行矢量化处理使用 vmovaps 来存储 B对齐),使用 vgather 来加载 A对齐

循环 2

·         源代码在加载 A 和加载/存储 C 上都具有单位步长访问

·         循环被剥离直到存储 C 对齐。剥离循环进行矢量化处理。

·         加载 A 可以是对齐或者非对齐。编译器为内核循环生成多版本代码

o    版本1用于加载 A非对齐 loadunpack、用于加载 C对齐 vaddps、用于存储 C对齐 vmovaps

o    版本2用于加载 A对齐 vmovaps、用于加载 C对齐 vaddps、用于存储 C对齐 vmovaps

·         剩余循环进行矢量化处理。

示例 1.2作为参数传输的对齐显形数组。

subroutine explicit2(A, B, C)

  real, intent(in), dimension(400,500) :: A

  real, intent(out), dimension(500)    :: B

  real, intent(inout), dimension(400)  :: C  

  !dir$ assume_aligned A(1,1):64

  !dir$ assume_aligned B(1):64

  !dir$ assume_aligned C(1):64

 

!...loop 1

  do i=1,500

    B(i) = A(3,i)

  end do

 

!...loop 2

  do i=1,400

    C(i) = C(i) + A(i, 400)

  end do

end

assume_aligned 指令用于宣告三个数组已经对齐。此指令提供的信息对于子例程中的两个循环都有效。

循环 1

·         不需要剥离循环,因为数组已经对齐。

·         使用面向存储 B vmovaps 和面向加载 A vgatherd 对内核循环进行矢量化处理。

·         剩余循环只有 4 个迭代在编译时已经知道没有进行矢量化处理因为其无法进行矢量化。

循环 2

·         不需要剥离循环。

·         使用面向所有三个内存访问的 vmovaps 对内核循环进行矢量化处理 两个加载和一个存储A C

·         不需要剩余循环(400*4 64 的倍数)。

示例 1.3作为参数传输的对齐显形数组。

subroutine explicit3(A, B, C)

  real, intent(in), dimension(400,500) :: A

  real, intent(out), dimension(500)    :: B

  real, intent(inout), dimension(400)  :: C

 

  !dir$ vector aligned

  do i=1,500

    B(i) = A(3,i)

  end do

 

  !dir$ vector aligned

  do i=1,400

   C(i) = C(i) + A(i, 400)

  end do

end

此方法与上面示例中介绍的方法相同。!dir$ 矢量对齐指令表示指定循环中的所有内存访问已经对齐。必须针对这两个循环重复该指令。

2: 可调整数组

与显形数组的描述类似针对子例程/函数生成的代码带有作为参数的可调整数组假设这些参数是连续的具有单位步长。在调用例程内的调用位置中,较大数组的一部分可能作为参数传输至调用过程。因此在调用位置,编译器生成代码以便在调用前将步长数组参数打包至连续的临时数组,并在调用后将其恢复至原始步长(将实际数据收集/分散至临时数组)。实际的子例程/函数调用使用临时数组作为参数。这会导致临时数组开销,以及执行收集/分散操作所需的内存移动的开销。

示例 2.1作为参数传输的 1D 可调整数组。

subroutine adjustable1(Y, Z, N)

 real, intent(inout), dimension(N) :: Y

 real, intent(in), dimension(N)    :: Z

 integer, intent(in)               :: N

 

 Y = Y + Z

 

 return

end

·         循环被剥离,直到存储 Y 对齐。

·         内核循环可以具有对齐或非对齐的加载 Z。编译器生成多版本代码:

o    版本 1用于加载 Z非对齐 loadunpack、用于加载 Y对齐 vaddps、用于存储 Y对齐 vmovaps

o    版本 2用于加载 Z对齐 vmovaps、用于加载 Y对齐 vaddps、用于存储 Y对齐 vmovaps

·         剩余循环进行矢量化处理。

示例 2.2作为参数传输的 2D 可调整数组。

subroutine adjustable2(Y, Z, M, N)

  real, intent(inout), dimension(M, N) :: Y

  real, intent(in), dimension(M, N)    :: Z

  integer, intent(in)                  :: M

  integer, intent(in)                  :: N

 

  Y = Y + Z

 

  return

end

·         编译器将两个数组对应的两个循环压缩为一个循环。

·         循环被剥离,直到存储 Y 对齐。

·         内核循环可以具有对齐或非对齐的加载 Z。编译器生成多版本代码:

o    版本 1用于加载 Z对齐 vloadunpack、用于加载 Y对齐 vaddps、用于存储 Y对齐 vmovaps

o    版本 2用于加载 Z对齐 vmovaps、用于加载 Y对齐 vaddps、用于存储 Y对齐 vmovaps

·         剩余循环进行矢量化处理。

示例 2.3作为参数传输的对齐 1D 可调整数组。

subroutine adjustable3(YY, ZZ, NN)

  real, intent(inout), dimension(NN) :: YY

  real, intent(in), dimension(NN)    :: ZZ

  integer, intent(in)                :: NN

  !dir$ assume_aligned YY:64,ZZ:64

 

  YY = YY + ZZ

 

  return

end

·         assume_aligned 指令可告诉此函数中的编译器YY ZZ 是指与 64 字节对齐的位置。

·         没有剥离循环因为 YY ZZ 已经对齐。

·         内核循环进行矢量化处理用于加载 ZZ对齐 vmovaps、用于加载 YY对齐 vaddps、用于存储 YY对齐 vmovaps

·         剩余循环进行矢量化处理(非对齐)。

示例 2.4对齐 2D 可调整数组示例。

subroutine adjustable4(YY, ZZ, MM, NN)

  real, intent(inout), dimension(MM, NN) :: YY

  real, intent(in), dimension(MM, NN)    :: ZZ

  integer, intent(in)                    :: MM

  integer, intent(in)                    :: NN

  !dir$ assume_aligned YY:64, ZZ:64

 

   YY = YY + ZZ

 

   return

end

·         assume_aligned 指令可告诉此函数中的编译器YY ZZ 是指与 64 字节对齐的位置。

·         编译器将两个循环压缩为一个循环。

·         没有剥离循环因为 YY ZZ 已经对齐。

·         内核循环进行矢量化处理用于加载 ZZ对齐 vmovaps、用于加载 YY对齐 vaddps、用于存储 YY对齐 vmovaps

·         剩余循环进行矢量化处理(非对齐)。

示例 2.5作为参数传输的对齐 1D 可调整数组。

subroutine adjustable5(YY, ZZ, NN)

  real, intent(inout), dimension(NN) :: YY

  real, intent(in), dimension(NN)    :: ZZ

  integer, intent(in)                :: NN

 

  !dir$ vector aligned

  YY = YY + ZZ

  return

end

·         vector aligned 指令可告诉此函数中的编译器YY ZZ 已经对齐。

·         没有剥离循环因为 YY ZZ 已经对齐。

·         内核循环进行矢量化处理用于加载 ZZ对齐 vmovaps、用于加载 YY对齐 vaddps、用于存储 YY对齐 vmovaps

·         剩余循环进行矢量化处理(非对齐)。

3: 假定形状数组

当假定形状数组用作过程参数时与上面示例中的可调整数组不同编程人员不会将该数组的大小作为显式参数传输。另外,针对函数/子例程生成的代码不会假定所传输的参数是连续的(单位步长)。参数可以是非连续的(步长,即其它数组的一部分)。在本例中,编译器不会在调用位置前/后生成代码将步长数组打包到临时的连续数组(也不会从该数组解压)。相反,它会针对每个数组的每一维生成代码,以便将上/下限和步长信息以及数组基本地址从调用方传输至被调用方。然后,调用方上生成的代码可使用这些上下限及步长信息,因此无需在调用方执行打包/解压操作。这对于调用和返回操作来说更加高效。

如果要单独编译调用方和被调用方,为了确保编译器知道要调用的函数中含有假定形状数组(因此不需要打包/解压,但需要传输上下限和步长信息),则需要在调用方一端进行接口声明(除非被调用方是一个内部过程,在这种情况下,接口是隐式的)。该接口声明相应的函数参数为假定形状数组。请注意,单位步长矢量化代码(适用于数组在运行时确实是连续的情况)的性能高于非单位步长矢量化代码(需要英特尔® 至强 融核协处理器上的收集/分散指令)。


连续和非连续数组都无须执行打包/解压即可传输至子例程/函数。因此,编译器在编译被调用方子例程时,不能盲目地为假定形状数组生成单位步长代码。在大多数情况下,编译器会生成多版本代码:
1) 对版本进行矢量化处理,以便在假定所有假定形状数组是单位步长的情况下对其访问,以及

2) 对版本进行矢量化处理以便在假定所有假定形状数组是非单位步长的情况下对其访问。

在过程中的运行时内对所有数组的步长进行检查,并选择矢量化区域的正确版本。

请注意第二种情况属于保守的低效运行fall-back。如果只有一个(可能会有多个)实际的假定形状数组参数是非单位步长,那么无论其余的参数是否是单位步长,系统将针对所有数组执行非单位步长矢量化的第二个版本。

示例 3.1作为参数传输的 1D 假定形状数组。

subroutine assumed_shape1(Y)

 real, intent(inout), dimension(:) :: Y

 

 Y = Y + 1

 

 return

end

·         针对数组步长生成多版本代码:

o    版本 1:矢量代码假定 Y 是单位步长。

§  剥离循环,直到 Y 对齐(收集/分散)

§  内核循环使用用于加载 Y对齐 vaddps、用于存储 Y对齐 vmovaps

§  剩余循环进行矢量化处理(收集/分散)。

o    版本 2:矢量代码假定 Y 不是单位步长。

§  没有剥离循环

§  内核循环使用用于加载 Y vgatherd、用于存储 Y vscatterd

§  剩余循环是标量。

示例 3.2作为参数的 2D 假定形状数组。

subroutine assumed_shape2(Z)

 real, intent(inout), dimension(:,:) :: Z

 

 Z = Z + 1

 

 return

end

·         2-D循环嵌套没有压缩至 1-D 循环。外层循环已经是非单位步长而且没有进行矢量化处理。

·         针对数组步长的内层循环生成两个版本 :

o    版本 1矢量代码假定 Z 是第一个维数内层循环维数的单位步长。

§  剥离循环,直到 Z 对齐(收集/分散)

§  内核循环使用用于加载 Z对齐 vaddps、用于存储 Z对齐 vmovaps

§  剩余循环进行矢量化处理(收集/分散)。

o    版本 2矢量代码假定 Z 是第一个维数内层循环维数的非单位步长。

§  没有剥离循环。

§  内核循环使用用于加载 Z vgather、用于存储 Z vscatter

§  两个剩余循环收集/分散带有一个标量循环和矢量大小的一半

示例 3.3作为参数的两个 1D 假定形状数组。

subroutine assumed_shape3(A, B)

 real, intent(out), dimension(:) :: A

 real, intent(in), dimension(:)  :: B

 

 A = B + 1

 

 return

end

·         针对数组步长生成多版本代码:

o    版本 1对带有单位步长的 A B 进行矢量化处理。

§  剥离循环,直到 A 对齐。

§  内核循环具有另外一个多版本代码:

o    版本 1a假定 B 对齐。

o    版本 1b假定 B 未对齐。

§  剩余循环进行矢量化处理。

o    版本 2既没有 A 也没有 B 单位步长的矢量代码。如果至少一个数组是非对齐步长,则会发生这种情况。

§  针对所有数组使用收集/分散指令。

§  剩余循环是标量。

示例 3.4作为参数的三个 1D 假定形状数组。

subroutine assumed_shape4(A, B, C)

 real, intent(out), dimension(:) :: A

 real, intent(in), dimension(:) :: B

 real, intent(in), dimension(:) :: C

 A = B + C

 

 return

end

·         针对数组步长生成多版本代码:

o    版本 1对带有单位步长的 AB C 进行矢量化处理。

§  剥离循环,直到 A 对齐。

§  内核循环具有多个版本:

o    版本 1a假定 B 对齐。假定 C 未对齐。

o    版本 1b假定 B 未对齐。假定 C 未对齐。

o    请注意多版本的第三层不是用于协调 C 来阻止过深的多版本。

o    版本 2带有所有数组非单位步长的矢量代码。如果至少一个数组是非对齐步长,则会发生这种情况。

§  针对所有数组使用收集/分散指令。

§  剩余循环是标量。

示例 3.5作为参数的三个对齐的 1D 假定形状数组。

subroutine assumed_shape5(A, B, C)

 real, intent(out), dimension(:) :: A

 real, intent(in), dimension(:)  :: B

 real, intent(in), dimension(:)  :: C

 !dir$ assume_aligned A:64,B:64,C:64

  

  A = B + C

 

  return

end

·         assume_aligned 指令用于表示三个数组已经对齐。

·         针对步长问题生成多版本代码:

o    版本 1带有 ABC 所有单位步长的矢量代码。

§  没有剥离循环,因为数组已经对齐。

§  内核循环不需要多版本因为所有数组已经宣称对齐。

§  剩余循环使用收集/分散指令。

o    版本 2没有数组单位步长的矢量代码。

§  使用收集/分散指令。(数组是否对齐不重要)

§  剩余循环是标量。

4. 假定大小数组

针对子例程/函数生成的代码带有假定大小数组参数假定这些参数是单位步长。与上面提到的将可调整数组作为参数传输相似,在每个调用位置,编译器生成代码以便在调用前将非单位步长数组参数打包至单位步长临时数组,并在调用后将其恢复至其原始步长位置。

示例 4.1作为参数传输的 1D 假定形状数组。

subroutine assumed_size1(Y)

 real, intent(inout), dimension(*) :: Y

 !The upper bound is known and hardcoded by the programmer

 !Y(:) = Y(:) + 1 => Illegal because size of Y is not known by the compiler

 

 Y(1:500) = Y(1:500) + 1

 

 return

end subroutine

·         剥离循环直到 Y 对齐使用收集/分散

·         剥离循环矢量化用于加载 Y对齐 vaddps、用于存储 Y对齐 vmovaps

·         剩余循环进行矢量化处理(使用收集/分散)。

示例 4.2作为参数的 2D 假定大小数组。

subroutine assumed_size2(Y)

 real, intent(inout), dimension(20,*) :: Y

 !Inner dimension size (i.e., 20) is provided to the compiler, so it can generate the

 !appropriate code for it. Outer dimension is unknown to the compiler, so it must be a

 !constant or a range of constants given by the programmer.

 !Y(:,:) => Illegal because 2nd dimension size of Y is unknown.

 

 Y(:,1:10) = Y(:,1:10) + 1

 

 return

end subroutine

·         剥离循环直到 Y 对齐使用收集/分散

·         内核循环矢量化用于加载 Y对齐 vaddps、用于存储 Y对齐 vmovaps

·         剩余循环进行矢量化处理(使用收集/分散)。

示例 4.3作为参数传输的对齐 1D 假定大小数组。

subroutine assumed_size3(Y)

 real, intent(inout), dimension(*) :: Y

 !dir$ assume_aligned Y:64

 

 Y(1:500) = Y(1:500) + 1

 

 return

end subroutine

·         assume aligned 指令可告诉此子例程中的编译器Y 已经和 64B 对齐。

·         没有剥离循环,Y 已经对齐。

·         内核循环矢量化用于加载 Y对齐 vaddps、用于存储 Y对齐 vmovaps

·         剩余循环只有 4 个迭代因此没有进行矢量化处理不划算。它是一个标量,可以完全展开。

示例 4.4作为参数传输的对齐 1D 假定大小数组。

subroutine assumed_size4(Y)

 real, intent(inout), dimension(*) :: Y

 !dir$ vector aligned

 

 Y(1:500) = Y(1:500) + 1

 

 return

end subroutine

·         vector aligned 指令可告诉此子例程中的编译器Y 已经对齐。

·         结果与上面示例中的相同。

5. 可分配数组

针对子例程/函数生成的代码带有可分配数组参数假定这些参数是单位步长。与上面提到的将可调整数组作为参数传输相似,在每个调用位置,编译器生成代码以便在调用前将非单位步长数组参数打包至单位步长临时数组,并在调用后将其恢复至其原始步长位置。

示例 5.1作为局部变量的 1D 可分配数组。

subroutine allocatable_array1(N)

 integer N

 real, allocatable :: Y(:)

 

  allocate (Y(N))

!..loop 1

  Y = 1.2

  call dummy_call()

 

!..loop 2

  Y = Y + 1

 

  call dummy_call()

  deallocate(Y)

  return

end

 

循环 1

·         剥离循环直到 Y 对齐。使用 vscatter 对剥离循环进行矢量化处理

·         使用 vmovaps 对内核循环进行矢量化处理(对齐)。

·         使用 vscatter 对剩余循环进行矢量化处理。

循环 2

·         剥离循环直到 Y 对齐。使用 vgather/vscatter 对剥离循环进行矢量化处理。

·         剥离循环矢量化用于加载 Y对齐 vaddps、用于存储 Y对齐 vmovaps

·         使用 vgather/vscatter 对剩余循环进行矢量化处理。

示例 5.2作为局部变量的 2D 可分配数组。

subroutine allocatable_array2(N)

  integer N

  real, allocatable :: Z(:,:)

 

  allocate(Z(N,N))

!..loop 1

  Z = 2.3

  call dummy_call()

 

!..loop 2

  Z = Z + 1

  call dummy_call()

 

  deallocate(Z)

  return

end

Loop 1:

·         剥离循环直到 Z 对齐。使用 vscatter 对剥离循环进行矢量化处理

·         使用面向存储 Z vmovaps 对内核循环进行矢量化处理对齐

·         使用 vscatter 对剩余循环进行矢量化处理。

循环 2

·         剥离循环直到 Z 对齐。使用 vgather/vscatter 对剥离循环进行矢量化处理。

·         内核循环矢量化用于加载 Z对齐 vaddps、用于存储 Z对齐 vmovaps

·         使用 vgather/vscatter 对剩余循环进行矢量化处理。

示例 5.3作为参数的 1D 可分配数组。

subroutine allocatable_array3(Y)

  real, intent(inout), allocatable, dimension(:) :: Y

!..loop 1

  Y = 1.2

  call dummy_call()

 

!..loop 2

  Y = Y + 1

  call dummy_call()

 

  return

end

循环 1

·         剥离循环直到 Y 对齐。使用分散指令对剥离循环进行矢量化处理。

·         使用面向存储 Y vmovaps 对内核循环进行矢量化处理对齐

·         使用 vscatter 对剩余循环进行矢量化处理。

循环 2

·         剥离循环直到 Y 对齐。使用收集/分散指令对剥离循环进行矢量化处理。

·         内核循环矢量化用于加载 Y对齐 vaddps、用于存储 Y对齐 vmovaps

·         使用 vgather/vscatter 对剩余循环进行矢量化处理。

示例 5.4作为参数的 2D 可分配数组。

subroutine allocatable_array5(Z)

  real, intent(inout), allocatable, dimension(:,:) :: Z

!..loop 1

  Z = 2.3

  call dummy_call()

 

!..loop 2

  Z = Z + 1

  call dummy_call()

 

  return

end

·         与上面的示例相同。剥离循环具有收集/分散指令。内核循环具有对齐访问。剩余循环具有收集/分散指令。

示例 5.5作为参数的对齐 1D 可分配数组。

subroutine allocatable_array6(Y)

  real, intent(inout), allocatable, dimension(:) :: Y

  !dir$ assume_aligned Y:64

  Y = 1.2

  call dummy_call()

  Y = Y + 1

  call dummy_call()

 

  return

end

·         ASSUME_ALIGNED 指令目前还无法正常工作编译器仍需剥离循环以便对齐 Y。此问题将在未来得到解决。

示例 5.6作为参数的对齐 1D 可分配数组。

subroutine allocatable_array6(Y)

  real, intent(inout), allocatable, dimension(:) :: Y

  !dir$ vector aligned

 

  Y = 1.2

  call dummy_call()

  !dir$ vector aligned

  Y = Y + 1

  call dummy_call()

 

  return

end

·         vector aligned 指令用于为两个循环生成对齐内存访问。

·         没有剥离循环因为编程人员已经告知 Y 会对齐。(编程人员需要确保 Y 确实对齐)

·         使用对齐的加载/存储指令对内核循环进行矢量化处理。

·         剩余循环进行矢量化处理。

6. 指针

Fortran 指针可能指向数组一个或多个维数上可能具有非单位步长的一个分段。因此,针对含有指针参数的例程生成的矢量代码具有多个版本:
1) 单位步长矢量化版本

2) 非单位步长矢量化版本

此外还可以针对数组对齐生成多个版本。

在指针对齐的情况下编译器将使用步长信息单位步长或非单位步长来删除两个版本中的一个因为其中的一个永远也不会被执行。但是,如果存在函数调用,该步长信息将会丢失,因为该函数可能会修改指针并使当前的步长信息失效。

示例 6.1作为参数传输的 1D 指针。

subroutine pointer1(Y)

  real, intent(inout), pointer, dimension(:) :: Y

 

  Y = Y + 1

 

  return

end

·         创建两个版本:

o    版本 1:带有单位步长访问的矢量化。

§  循环剥离直到 Y 对齐使用 vgather/vscatter

§  使用 vmovaps 对内核循环进行矢量化处理(对齐)。

§  使用 vscatter 对剩余循环进行矢量化处理。

o    版本 2带有非单位步长访问的矢量化。使用 vgather/vscatter

示例 6.2作为参数传输的三个 1D 指针。

subroutine pointer2(A,B,C)

  real, intent(inout), pointer, dimension(:) :: A,B,C

 

  A = A + B + C + 1

 

  return

end

·         循环被划分为两个循环:

o    第一个循环写入与 64B 对齐的临时数组 A_tmp

o    第二个循环从 A_tmp对齐复制到 A对齐情况未知

·         此外我们还针对这两个循环的单位步长与非单位步长提供了多版本代码。

o    Loop 2a(A_tmp = A + B + C + 1)

§  版本 1矢量化假定 AB C 全是单位步长。

o    没有剥离循环,A_tmp 已经对齐。

o    对内核循环进行矢量化处理假定 AB C 对齐。

o    剩余循环未对齐(使用 vgather/vscatter)。

§  版本 2矢量化假定 AB C 全是非单位步长。

o    剥离循环是标量。

o    内核循环使用 vgather/vscatter

o    剩余循环是标量。

o    Loop 2b(A = A_tmp)

§  版本 1矢量化假定 A 是单位步长。已经知道 A_tmp 是单位步长且已经对齐。

o    剥离循环是标量。剥离直到 A 对齐。

o    对于内核循环:

o    版本 1a矢量化假定 A A_tmp 已经对齐。即剥离循环不执行任何迭代

o    版本 1b矢量化假定 A 对齐A_tmp 未对齐。

o    剩余循环是标量。

§  版本 2:矢量化,假定 A 是非单位步长。

o    剥离循环是标量。

o    内核循环矢量化用于加载 A_tmp对齐 vmovaps、用于存储 A非对齐和步长 vscatter

o    剩余循环是标量。

示例 6.3编译器传输的指针分配带有步长信息

module mod1

  implicit none

  real, target, allocatable :: A1(:,:)

  real, pointer :: Z1(:,:)

contains

  subroutine pointer_array3(N)

    integer N

    Z1 => A1(:, 1:N:2)

    Z1 = Z1 + 1

    return

  end subroutine

end module

·         Z1 A1 的一部分。其内部维数是连续的,但是外部维数是非连续的。

·         此信息在循环内部使用而且仅生成假定是连续布局的内部循环的矢量版本

·         剥离循环直到 A1 对齐。使用 vgather/vscatter 对剥离循环进行矢量化处理。

·         使用对齐的加载/存储指令对内核循环进行矢量化处理。

·         使用 vgather/vscatter 对剩余循环进行矢量化处理。

示例 6.4由于函数调用造成步长信息丢失的指针示例。

module mod2

  implicit none

  real, target, allocatable :: A2(:,:)

  real, pointer :: Z2(:,:)

contains

  subroutine pointer_array4(N)

    integer N

    Z2 => A2(:, 1:N:2)

    !..loop 1

    Z2 = 2.3

   

    dummy_call()

   

    !..loop 2

    Z2 = Z2 + 1

   

    return

  end subroutine

end module

·         Z2 A2 的一部分。其内部维数是连续的,但是外部维数是非连续的。

·         循环 1

o    A2 步长信息在循环内部使用而且仅生成假定是连续布局的内部循环的矢量版本

o    剥离循环直到 A2 对齐。使用 vgather/vscatter 对剥离循环进行矢量化处理。

o    使用对齐的加载/存储指令对内核循环进行矢量化处理。

o    使用 vgather/vscatter 对剩余循环进行矢量化处理。

·         循环 2

o    dummy_call() 函数调用可能会改变 A2 所指向的位置。因此 A2 上的步长信息丢失。

o    针对 A2 步长生成多版本代码:

§  版本 1带有 A2 单位步长的矢量代码面向内部循环

o    剥离循环直到 A2 对齐。使用收集/分散指令对剥离循环进行矢量化处理。

o    使用对齐的加载/存储指令对内核循环进行矢量化处理。

o    剩余循环进行矢量化处理(使用收集/分散)。

§  版本 2带有 A2 非单位步长的矢量代码面向内部循环

o    使用收集/分散指令对内核循环进行矢量化处理。

o    剩余循环具有两个版本带有收集/分散指令的矢量化版本和一个标量版本。

7. 间接数组访问

间接数组访问在带有稀疏数组的应用、基于自适应网格的应用以及许多 N 体应用中十分常见。在此类情况中,用户可以顺序访问一个索引数组并将其用作基本数组/矩阵的补偿。内存访问是非连续的,需要在英特尔® 至强 融核协处理器上使用收集/分散指令。

示例 7.1使用 Fortran 数组符号对数组进行间接访问。

subroutine indirect1(A, B, ind, N)

  real A(N),B(N)

  integer N

  integer ind(N)

 

  A(ind(:)) = A(ind(:)) + B(ind(:)) + 1

 

  return

end

·         通过索引数组 'ind' 对数组 A B 进行间接访问。

·         Fortran 数组符号语义要求执行该分配语句的结果与评估所有数组元素的右侧并将结果分配给目标相当。由于循环的流动依赖于 A 上此分配的右侧与左侧之间,该分配将进行矢量化处理,具体的方式是使用一个临时数组来存储整个右侧的结果,然后使用第二个循环将其复制到 A() 数组。因此,编译器生成两个循环:

o    T(:) = A(ind(:)) + B(ind(:)) + 1

o    A(ind(:)) = T(:).

·         循环 1

o    没有剥离循环。T 已经作为对齐进行分配。

o    内核循环进行矢量化处理面向 A B 的收集、面向 ind 的未对齐加载以及面向 T 的对齐存储。

o    剩余循环进行矢量化处理面向 A B 的收集、面向 T 的分散。

·         循环 2

o    没有剥离循环。T 已经对齐。

o    内核循环进行矢量化处理面向 T 的对齐加载、面向 A 的分散以及面向 ind 的非对齐加载。

o    剩余循环是标量。

示例 7.2使用 Fortran do 循环对数组进行间接访问。

subroutine indirect2(A, B, ind, N)

  real A(N),B(N)

  integer N

  integer ind(N)

  integer i

  !dir$ ivdep

 

  do i=1,N

    A(ind(i)) = A(ind(i)) + B(ind(i)) + 1

  end do

 

  return

end

·         通过索引数组 ind 对数组 A B 进行间接访问。

·         do-loop 语义与上面的 Fortran 数组符合不同。do-loop 表示循环迭代的连续执行,这会防止对该循环进行矢量化处理,这是因为循环的流动依赖于 A 上此分配的右侧与左侧之间。但是,编程人员提供的 ivdep 指令表示忽略循环流动依赖性是安全的,可以对该代码进行矢量化处理。

·         根据该循环和指令的语义编译器不需要生成临时数组和两个循环。

o    循环被剥离直到 ind 对齐。剥离循环进行矢量化处理面向加载 AB ind vgather、面向存储 A vscatter

o    内核循环进行矢量化处理面向加载 AB vgather、面向 ind 的对齐加载、面向存储 A vscatter

o    剩余循环进行矢量化处理面向加载 AB vgather、面向 ind 的对齐屏蔽加载、面向存储 A vscatter

8. Fortran90 模块中的全局数组

模块是定义全局数据的有效方法。您可以在模块中指定对齐语句以便在全局范围内对齐数组
 
示例 8.1在模块中声明全局数组带有已知的大小
 
module mymod
  !dir$ attributes align:64 ::
a
  !dir$ attributes align:64 ::
b
  real (kind=8) ::
a(1000), b(1000)
end module mymod
 
subroutine add_them()
use mymod
implicit none
 
!  array syntax shown.

!...No explicit directive needed to tell the compiler that A and B
!  are aligned, the USE brings that information
 
  a = a + b
end subroutine add_them
 

模块中的 attributes align 指令用于告诉编译器A B 64B 对齐。

未生成剥离循环A B 已经对齐。请注意,循环的前面不需要单独的 vector-aligned 指令。
使用 vmovapd 对内核循环进行矢量化处理(对齐)。


示例 8.2在模块中声明全局可分配变量但是在其他地方分配。

 

module mymod
  real, allocatable ::
a(:), b(:)
end module mymod
 
subroutine add_them()
use mymod
implicit none
 
!dir$ vector aligned
  a = a + b
end subroutine add_them


模块中的 attributes align 指令还不够因为在调用 ALLOCATE A B 的实际分配会在晚些时候发生。

如果在此处使用 vector aligned 指令则不会生成剥离循环编译器假定 A B 已经对齐。如果删除 vector aligned 指令,编译器就没有正确的对齐信息,这时便会生成剥离循环来对齐数组。

使用 vmovapsvaddps 对内核循环进行矢量化处理对齐

请注意更改模块来添加 attributes align 语句不会提供帮助

module mymod
  real, allocatable ::
a(:), b(:)
  !dir$ attributes align:64 :: a                                                             
  !dir$ attributes align:64 :: b                                                             
end module mymod

该模块定义并未说明分配的数据指针已经和 64 字节对齐。[考虑一下指针变量位于 64 字节边界,而且那里存储的指针的值可能是(或者不是)64 字节对齐]

9. 使用 -align array64byte 选项来对齐所有数组


编译器选项 -align arraynbyte 支持所有 Fortran 数组 {COMMON 中的数组除外}。它会在 n 字节边界对齐数组的开头。n 可以是 8163264128 256默认值是 8。数组在其元素之间没有填充(padding)。这必须使用固定限制,假定-shape –size、自动、可分配或指针属性{这不适用于 Cray 指针数组。这些数组没有对齐控制功能}
 

Fortran 数组提供了三种内存类型
a)      静态内存内存中数组的开头在 n 字节边界对齐

b)      堆栈内存局部、自动化、临时堆栈中数组的开头在 n 字节边界对齐

c)      堆内存可分配或指针属性 {不是 Cray 指针数组} – 内存通过调用 aligned_malloc ALLOCATE 语句来分配因此堆中数组的开头在 n 字节边界对齐。

-align arraynbyte 是编译中对齐所有数组的一种方式。如果您希望对齐单独的数组,请针对每个数组使用 ALIGN 属性。此外编程人员还应使用指令 (!dir$ vector aligned, !dir$ assume_aligned) 在每个循环之前明确地通知编译器可通过对齐的方式访问哪些数组。

要点

本文举例说明了 Fortran 中不同的数组类型及其作为局部变量、函数/子例程参数的应用并且提供了有关 Fortran 指针及其使用情况的示例。本文介绍了编译器如何对不同的数组数据类型和参数进行矢量化处理。其中包括关于高级源代码的示例,并详细解释了编译器针对这些情况所生成的代码。

本文列举了许多示例具体的要点都有哪些

1.    数组间接访问的成本较高并且会降低代码的效率。是的,编译器仍可使用 vgather/vscatter 对具有间接内存参考的循环进行矢量化处理。这并不意味着代码具有较高的效率!!!高效的代码是单位步长访问。如果你的代码使用指针或间接参考,则无论是否创建矢量代码,都不会达到最优的状态。如果可能,请避免间接内存参考,或者弄清楚编译器不能使代码的效率和性能增加

2.    避免使用临时数组来传输参数可以使用显式接口和假定形状数组传输连续数组数据来实现这一点。更好的是,将数据放在模块和 USE 中,而不是明确地传输参数

3.    尽管 Fortran 指针的限制远远高于 C 指针但它们仍会同时使用。表达式左侧 (LHS) 和右侧 (RHS) 的基于指针的变量可能发生重叠,这会导致创建临时数组来保存 RHS 表达式,这是因为它会在存储到 LHS 之前进行评估。如果可能,请避免指针并使用可分配数组。

下一步

要在英特尔® 至强 融核架构上成功调试您的应用请务必通读此指南并点击文中的超链接查看相关内容。本指南提供了实现最佳应用性能所要执行的步骤。

返回到主章节“矢量化要素”。

 

Reportez-vous à notre Notice d'optimisation pour plus d'informations sur les choix et l'optimisation des performances dans les produits logiciels Intel.