Fortran の配列データおよび引数とベクトル化

HPCインテル® Fortran コンパイラーインテル® Parallel Studio XE

この記事は、インテル® デベロッパー・ゾーンに掲載されている「Fortran Array Data and Arguments and Vectorization」(http://software.intel.com/en-us/articles/fortran-array-data-and-arguments-and-vectorization) の日本語参考訳 (一部) です。


ベクトル化の基本 – Fortran の配列データおよび引数とベクトル化

ここでは、Fortran のさまざまな配列型の例と、ローカル変数、および関数/サブルーチン引数としての使用法を説明します。また、Fortran ポインターの使用法と例にも紹介します。さらに、さまざまな配列データ型と引数が、コンパイラーによってどのようにベクトル化されるか、Fortran の例を使って、コンパイラーにより生成されるコードについて説明します。

Fortran の配列データと引数、およびベクトル化の例

最近の Fortran 言語には、さまざまな配列型と、配列のサブセクションを関数の引数として渡す (または Fortran ポインターで指す) 配列セクション機能が実装されています。配列を処理するループに対して、コンパイラーはユニットストライド方式のベクトルコード、非ユニットストライド方式のスカラーコード、または (ランタイムに実行するコードを決定する) 複数バージョンのコードを生成することができます。コンパイラーは、配列セクションが使用されている場合や、配列が関数の引数として渡されている場合でも、プログラマーが指定した配列型に基づいてループのコードを生成します。

SIMD (Single Instruction, Multiple Data) 命令を使用するため、非ユニットストライド方式でメモリーアクセスを行うコードには、ギャザー/スキャッター命令 (vgather/vscatter) を使用します。ユニットストライド方式でメモリーアクセスを行うコードは、より効率良いロードとストアを使用できます。このようなコードは、ターゲットのメモリー位置のアドレス・アライメントに基づいて、アライメントされたロード/ストア命令 (vmovaps または同等な命令) あるいはアライメントされていないロード/ストア命令 (vloadunpack/vpackstore) を使用します。

コンパイラーがユニットストライド方式のコードをベクトル化する場合、優れたパフォーマンスが得られるように (例えば、アライメントされたロード/ストア命令を使用して) 配列をアライメントすることも重要です。プログラマーは、コンパイラー・オプション (-align array64byte) またはディレクティブ (!dir$ attribute align) を使用して、配列にアライメントされたメモリーを割り当てるようにコンパイラーに指示することができます。しかし、これだけではアライメントされたループのベクトルコードを生成することはできません。プログラマーは、ディレクティブ (!dir$ vector aligned、!dir$ assume_aligned) を使用して、どの配列がアライメントされた形式でアクセスされるか、各ループについてコンパイラーに伝える必要があります。このガイドには、「ベクトル化の可能性を高めるデータ・アライメント」に記述されている、アライメントに関する詳細情報が含まれています。

アライメントされていない配列を含むループについては、コンパイラーは通常「ピールループ」を生成します。ピールループはメインループに先行するループで、配列の 1 つがアライメントされるまで配列のデータ要素を処理します (ベクトル化されたピールループはギャザー/スキャッター命令を使用します)。ピールループの後、この配列にアライメントされたメモリーアクセスを行う「カーネルループ」がアライメントされたデータを処理します。ピールループとカーネルループに続いて、すべての反復が終了するまで、配列の最後でリマインダー・ループが残りのアライメントされていないデータを処理します (ベクトル化されたリマインダー・ループはギャザー/スキャッター命令を使用します)。カーネルループでは、コンパイラーは 2 つ目の配列および複数バージョンのループ (この配列にアライメントされたアクセスを行うループとアライメントされていないアクセスを行うループ) のアライメントを動的にチェックするコードを生成します。

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 

ループ 1:

  • ソースコードには、B へのユニット・ストライド・アクセスと A への非ユニット・ストライド・アクセスが含まれます。
  • ループは、B がアライメントされるまでピールされます。
  • ピールループはベクトル化されますが、A のロード (アライメントされていない) に vgather を使用し、B のストア (アライメントされていない) に vscatter を使用します。
  • カーネルループは、B のストア (アライメントされている) に vmovaps を使用し、A のロード (アライメントされていない) に vgather を使用します。
  • リマインダー・ループはベクトル化され、B のストア (アライメントされている) に vmovaps を使用し、A のロード (アライメントされていない) に vgather を使用します。

ループ 2:

  • ソースコードには、A のロードと C のロード/ストアの両方でユニット・ストライド・アクセスが含まれます。
  • ループは、C のストアがアライメントされるまでピールされます。ピールループはベクトル化されます。
  • A のロードはアライメントされている可能性も、されていない可能性もあります。コンパイラーは、複数バージョンのカーネル・ループ・コードを生成します。
    • バージョン 1: A のロード (アライメントされていない) に vloadunpack を使用し、C のロード (アライメントされている) に vaddps を使用し、C のストア (アライメントされている) に vmovaps を使用します。
    • バージョン 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 ディレクティブを使用して 3 つの配列がアライメントされていることを示します。このディレクティブによって提供される情報は、サブルーチン内の両方のループで有効です。

ループ 1:

  • 配列はすでにアライメントされているため、ピールループはありません。
  • カーネルループは、B のストアに vmovaps を使用し、A のロードに vgatherd を使用してベクトル化されます。
  • リマインダー・ループの反復は 4 回のみです (コンパイル時に判明)。ベクトル化は効率的ではないため、ベクトル化されません。

ループ 2:

  • ピールループはありません。
  • カーネルループは、すべてのメモリーアクセス (A と C の 2 つのロードと 1 つのストア) に vmovaps を使用してベクトル化されます。
  • リマインダー・ループは不要です (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$ vector aligned ディレクティブは、指定されたループ内のすべてのメモリーアクセスがアライメントされていることを示します。このディレクティブは、ループごとに指定する必要があります。

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 のロードは、アライメントされている可能性も、されていない可能性もあります。コンパイラーは複数バージョンのコードを生成します。
    • バージョン 1: Z のロード (アライメントされていない) に vloadunpack を使用し、Y のロード (アライメントされている) に vaddps を使用し、Y のストア (アライメントされている) に vmovaps を使用します。
    • バージョン 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
  • 2 つの配列次元に対応する 2 つのループは、コンパイラーによって単一のループにコラプスされます。
  • ループは、Y のストアがアライメントされるまでピールされます。
  • カーネルループの Z のロードは、アライメントされている可能性も、されていない可能性もあります。コンパイラーは複数バージョンのコードを生成します。
    • バージョン 1: Z のロード (アライメントされていない) に vloadunpack を使用し、Y のロード (アライメントされている) に vaddps を使用し、Y のストア (アライメントされている) に vmovaps を使用します。
    • バージョン 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 バイトでアライメントされた場所を参照することをコンパイラーに知らせます。
  • 2 つのループは、コンパイラーによって単一のループにコラプスされます。
  • 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: 形状引き継ぎ配列

形状引き継ぎ配列がプロシージャーのパラメーターとして使用される場合、上記の整合配列の例とは異なり、プログラマーによって配列サイズが明示的なパラメーターとして渡されません。さらに、生成されるコードは、渡されるパラメーターが連続している (ユニットストライドである) と仮定しません。パラメーターは非連続 (ストライド、つまりほかの配列のセクション) の可能性があります。この場合、コンパイラーは、呼び出し位置の前/後に連続する一時配列との間でストライド配列をパック/アンパックするコードを生成しません。代わりに、呼び出し元から呼び出し先へ、配列のベースアドレスとともに、各配列の各次元の上限/下限とストライド情報を渡すコードを生成します。呼び出し先の生成コードはこの情報を使用するため、呼び出し元でパック/アンパックが不要になります。これは呼び出しとリターンの両方にとって効率的です。

呼び出し元と呼び出し先が別々にコンパイルされる場合、呼び出される関数が形状引き継ぎ配列を含むこと (したがって、パック/アンパックは不要だが、境界とストライドを渡す必要があること) をコンパイラーに知らせるため、呼び出し元でインターフェイス宣言が必要です (ただし、呼び出し先が内部プロシージャーの場合は、インターフェイスが暗黙的になるため宣言は不要です)。このインターフェイスは、対応する関数引数が形状引き継ぎ配列でることを宣言します。ユニットストライドのベクトル化コードのパフォーマンスは (実行時に配列が実際に連続している場合)、非ユニットストライドのベクトル化コードよりも優れています。

形状引き継ぎ配列で Fortran 2008 CONTIGUOUS 属性を指定すると、データが連続するブロックを占有していることをコンパイラーに知らせることができます。これにより、コンパイラーは形状引き継ぎ配列を最適化できます。この属性については、後述のセクションで詳しく説明します。

連続/非連続配列は、どちらもパック/アンパックせずにこれらのサブルーチン/関数に渡すことができます。そのため、コンパイラーは呼び出し先のサブルーチンをコンパイルする際に、形状引き継ぎ配列のユニット・ストライド・コードを盲目的に生成できません。代わりに、ほとんどの場合、コンパイラーは複数バージョンのコードを生成します。

  1. すべての形状引き継ぎ配列がユニットストライドであると仮定してアクセスするベクトル化されたバージョン
  2. すべての形状引き継ぎ配列が非ユニットストライドであると仮定してアクセスするベクトル化されたバージョン
    実行時にプロシージャー内ですべての配列のストライドがチェックされ、適切なベクトル化バージョンが選択されます。

2 つ目のバージョンは、保守的なフォールバック・ケースです。非ユニットストライドの実形状引き継ぎ配列引数が 1 つでもある場合は、残りの引数がユニットストライドであっても、すべての配列で 2 つ目の非ユニットストライドのベクトル化バージョンが実行されます。

例 3.1: 1D 形状引き継ぎ配列をパラメーターとして渡す

subroutine assumed_shape1(Y)

 real, intent(inout), dimension(:) :: Y
 
 Y = Y + 1
 
 return
end
  • 配列ストライドの複数バージョンのコードが生成されます。
    • バージョン 1: Y がユニットストライドであると仮定してベクトル化されたコード。
      • ループは、Y がアライメントされるまで、ギャザー/スキャッターを使用してピールされます。
      • カーネルループは、Y のロード (アライメントされている) に vaddps を使用し、Y のストア (アライメントされている) に vmovaps を使用します。
      • リマインダー・ループは、ギャザー/スキャッターを使用してベクトル化されます。
    • バージョン 2: Y が非ユニットストライドであると仮定してベクトル化されたコード。
      • ピールループはありません。
      • カーネルループは、Y のロードに vgatherd を使用し、Y のストアに vscatterd を使用します。
      • リマインダー・ループはスカラーです。

例 3.2: 2D 形状引き継ぎ配列をパラメーターとして渡す

subroutine assumed_shape2(Z)

 real, intent(inout), dimension(:,:) :: Z
 
 Z = Z + 1
 
 return
end
  • 2D ループは 1D ループにコラプスされません。外側のループは非ユニットストライドであり、ベクトル化されません。
  • 内側のループの配列ストライドに対し 2 つのバージョンが生成されます。
    • バージョン 1: Z の最初の次元 (内側のループの次元) がユニットストライドであると仮定してベクトル化されたコード。
      • ループは、Z がアライメントされるまで、ギャザー/スキャッターを使用してピールされます。
      • カーネルループは、Z のロード (アライメントされている) に vaddps を使用し、Z のストア (アライメントされている) に vmovaps を使用します。
      • リマインダー・ループは、ギャザー/スキャッターを使用してベクトル化されます。
    • バージョン 2: Z の最初の次元 (内側のループの次元) が非ユニットストライドであると仮定してベクトル化されたコード。
      • ピールループはありません。
      • カーネルループは、Z のロードに vgather を使用し、Z のストアに vscatter を使用します。
      • リマインダー・ループは、半分のベクトルサイズのギャザー/スキャッターとスカラーループの 2 つのバージョンが生成されます。

例 3.3: 2 つの 1D 形状引き継ぎ配列をパラメーターとして渡す

subroutine assumed_shape3(A, B)

 real, intent(out), dimension(:) :: A
 real, intent(in), dimension(:)  :: B
 
 A = B + 1
 
 return
end
  • 配列ストライドの複数バージョンのコードが生成されます。
    • バージョン 1: A と B が両方ともユニットストライドであると仮定してベクトル化されたコード。
      • ループは、A がアライメントされるまでピールされます。
      • カーネルループも、複数バージョンのコードが生成されます。
        • バージョン 1a: B がアライメントされていると仮定。
        • バージョン 1b: B がアライメントされていないと仮定。
      • リマインダー・ループはベクトル化されます。
    • バージョン 2: A と B がどちらもユニットストライドではないと仮定してベクトル化されたコード。いずれかの配列が非ユニットストライドの場合、このケースに該当します。
      • すべての配列にギャザー/スキャッターを使用します。
      • リマインダー・ループはスカラーです。

例 3.4: 3 つの 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
  • 配列ストライドの複数バージョンのコードが生成されます。
    • バージョン 1: A、B、C がすべてユニットストライドであると仮定してベクトル化されたコード。
      • ループは、A がアライメントされるまでピールされます。
      • カーネルループも、複数バージョンのコードが生成されます。
        • バージョン 1a: B はアライメントされており、C はアライメントされていないと仮定。
        • バージョン 1b: B とC はどちらもアライメントされていないと仮定。
        • バージョンが深くなりすぎるため、C をアライメントする第 3 レベルの複数バージョンは作成されません。
    • バージョン 2: すべての配列が非ユニットストライドであると仮定してベクトル化されたコード。いずれかの配列が非ユニットストライドの場合、このケースに該当します。
      • すべての配列にギャザー/スキャッターを使用します。
      • リマインダー・ループはスカラーです。

例 3.5: 3 つのアライメントされた 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 ディレクティブを使用して 3 つの配列がすべてアライメントされていることを示します。
  • 配列ストライドの複数バージョンのコードが生成されます。
    • バージョン 1: A、B、C がすべてユニットストライドであると仮定してベクトル化されたコード。
      • 配列はすでにアライメントされているため、ピールループはありません。
      • すべての配列がアライメントされているため、カーネルループは複数バージョンを必要としません。
      • リマインダー・ループはギャザー/スキャッターを使用します。
    • バージョン 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

ループ 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
  • 「インテル® Fortran コンパイラーでの Fortran 割付け配列とポインターのアライメント」 (https://software.intel.com/en-us/articles/alignment-of-fortran-allocatable-arrays-pointers-in-intel-fortran-compiler) も参照してください。

例 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 ディレクティブを使用して、2 つのループのアライメントされたメモリーアクセスを生成します。
  • Y はプログラマーによってアライメントされていると示されているため、ピールループはありません (プログラマーは、Y が実際にアライメントされていることを保証する必要があります)。
  • カーネルループは、アライメントされたロード/ストアを使用してベクトル化されます。
  • リマインダー・ループはベクトル化されます。

6. ポインター

Fortran ポインターは、1 つ以上の次元で非ユニットストライドの可能性がある配列セクションをポイントすることができます。そのため、ポインター引数を持つルーチンでは、複数バージョンのベクトルコードが生成されます。

  1. ユニットストライドのベクトル化されたバージョン
  2. 非ユニットストライドのベクトル化されたバージョン
    さらに、配列のアライメントに対しても複数バージョンが生成されます。

ポインター割り当てでは、コンパイラーはストライド情報 (ユニットストライドか非ユニットストライドか) を使用して 2 つのバージョンのいずれかを選択します。しかし、関数呼び出しがある場合、関数がポインターを変更し、現在のストライド情報が無効になる可能性があるため、このストライド情報は失われます。

ポインター配列で Fortran 2008 CONTIGUOUS 属性を指定すると、データが連続するブロックを占有していることをコンパイラーに知らせることができます。この属性については、後述のセクションで詳しく説明します。

例 6.1: 1D ポインターをパラメーターとして渡す

subroutine pointer1(Y)
  real, intent(inout), pointer, dimension(:) :: Y
  
  Y = Y + 1
  
  return
end
  • 2 つのバージョンが生成されます。
    • バージョン 1: ユニット・ストライド・アクセスを使用してベクトル化されたコード。
      • ループは、Y がアライメントされるまで、vgather/vscatter を使用してピールされます。
      • カーネルループは、vmovaps (アライメントされている) を使用してベクトル化されます。
      • リマインダーループは、vscatter を使用してベクトル化されます。
    • バージョン 2: 非ユニット・ストライド・アクセスを使用してベクトル化されたコード。vgather/vscatter を使用します。

例 6.2: 3 つの 1D ポインターをパラメーターとして渡す

subroutine pointer2(A,B,C)

  real, intent(inout), pointer, dimension(:) :: A,B,C
  
  A = A + B + C + 1
  
  return
end
  • ループは 2 つのループに分割されます。
    • 1 つ目のループは、64B でアライメントされた一時配列 A_tmp へ書き込みます。
    • 2 つ目のループは、A_tmp (アライメントされている) から A (アライメントされているかどうかは不明) へコピーします。
  • また、この 2 つのループにはそれぞれ、ユニットストライドと非ユニットストライドに対応した複数バージョンが生成されます。
    • ループ 2a: (A_tmp = A + B + C + 1)
      • バージョン 1: A、B、C がすべてユニットストライドであると仮定してベクトル化されたコード。
        • A_tmp はすでにアライメントされているため、ピールループはありません。
        • カーネルループは、A、B、C がアライメントされていないと仮定してベクトル化されます。
        • リマインダー・ループはアライメントされず、vgather/vscatter を使用します。
      • バージョン 2: A、B、C がすべて非ユニットストライドであると仮定してベクトル化されたコード。
        • ピールループはスカラーです。
        • カーネルループは、vgather/vscatter を使用します。
        • リマインダー・ループはスカラーです。
    • ループ 2b: (A = A_tmp)
      • バージョン 1: A がユニットストライドであると仮定してベクトル化されたコード。A_tmp はユニットストライドで、アライメントされていることがすでに分かっています。
        • ピールループはスカラーです。ループは、A がアライメントされるまでピールされます。
        • カーネルループ:
          • バージョン 1a: A と A_tmp がアライメントされていると仮定してベクトル化されたコード (つまり、ピールループの反復は全く実行されない)。
          • バージョン 1b: A はアライメントされており、A_tmp はアライメントされていないと仮定してベクトル化されたコード
        • リマインダー・ループはスカラーです。
      • バージョン 2: A が非ユニットストライドであると仮定してベクトル化されたコード。
        • ピールループはスカラーです。
        • カーネルループは、A_tmp のロード (アライメントされている) に vmovaps を使用し、A のストア (ストライドでアライメントされていない) に vscatter を使用してベクトル化されます。
        • リマインダー・ループはスカラーです。

例 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:
    • A2 のストライド情報がループで使用され、連続する内側のループのみベクトルバージョンが生成されます。
    • ループは、A2 がアライメントされるまでピールされます。ピールループは、vgather/vscatter を使用してベクトル化されます。
    • カーネルループは、アライメントされたロード/ストアを使用してベクトル化されます。
    • リマインダー・ループは、vgather/vscatter を使用してベクトル化されます。
  • ループ 2:
    • dummy_call() 関数呼び出しは、A2 が指す場所を変更する可能性があります。これにより、A2 のストライド情報は失われます。
    • A2 のストライドの複数バージョンのコードが生成されます。
      • バージョン 1: A2 (内側のループ) にユニット・ストライド・アクセスするベクトルコード。
        • ループは、A2 がアライメントされるまでピールされます。ピールループは、ギャザー/スキャッターを使用してベクトル化されます。
        • カーネルループは、アライメントされたロード/ストアを使用してベクトル化されます。
        • リマインダー・ループは、ギャザー/スキャッターを使用してベクトル化されます。
      • バージョン 2: A2 (内側のループ) に非ユニット・ストライド・アクセスするベクトルコード。
        • カーネルループは、ギャザー/スキャッターを使用してベクトル化されます。
        • リマインダー・ループは、ギャザー/スキャッターを使用したてベクトル化されたバージョンとスカラーバージョンの 2 つが生成されます。

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
  • 配列 A と B は、インデックス配列 ind を介して間接的にアクセスされます。
  • Fortran 配列表記のセマンティクスでは、この代入文の実行結果が、右辺のすべての配列要素を評価して結果をターゲットに代入した場合と等しくなければなりません。この代入文の右辺と左辺の間には A のループ伝播フロー依存があるため、最初に一時配列に右辺全体の結果を格納して、次に 2 つ目のループでそれを A() 配列にコピーすることでベクトル化できます。結果として、コンパイラーは 2 つのループを生成します。
    • T(:) = A(ind(:)) + B(ind(:)) + 1
    • A(ind(:)) = T(:).
  • ループ 1:
    • ピールループはありません。T はすでにアライメント済みとして割り当てられています。
    • カーネルループは、A と B にギャザーを使用し、ind にアライメントされていないロードを使用し、T にアライメントされたストアを使用してベクトル化されます。
    • リマインダー・ループは、A と B にギャザーを使用し、T にスキャッターを使用してベクトル化されます。
  • ループ 2:
    • ピールループはありません。T はすでにアライメントされています。
    • カーネルループは、T にアライメントされたロードを使用し、A にスキャッターを使用し、ind にアライメントされていないロードを使用してベクトル化されます。
    • リマインダー・ループはスカラーです。

例 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
  • 配列 A と B は、インデックス配列 ind を介して間接的にアクセスされます。
  • DO ループのセマンティクスは、上記の Fortran 配列表記とは異なります。DO ループは、ループの反復がシーケンシャルに実行されることを意味します。これは、代入文の右辺と左辺の間に A のループ伝播フロー依存があるこのループのベクトル化を妨げます。しかし、プログラマーによって指定された ivdep ディレクティブは、ループ伝播フロー依存を無視してこのコードをベクトル化しても安全であることを示しています。
  • これにより、コンパイラーは一時配列と 2 つのループを生成する必要がありません。
    • ループは、ind がアライメントされるまでピールされます。ピールループは、A、B、ind のロードに vgather を使用し、A のストアに vscatter を使用してベクトル化されます。
    • カーネルループは、A と B に vgather を使用し、ind にアライメントされたロードを使用し、A のストアに vscatter を使用してベクトル化されます。
    • リマインダー・ループは、A と B のロードに vgather を使用し、ind にアライメントされたマスク付きロードを使用し、A のストアに vscatter を使用してベクトル化されます。

8. Fortran90 モジュールのグローバル配列

モジュールは、グローバルデータの定義に有効な手法を提供します。モジュール内で align 節を指定して、配列をグローバルにアライメントできます。

例 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

モジュール内で 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

A と B の実際の割り当ては、後で ALLOCATE が呼び出されたときに行われるため、モジュール内で align ディレクティブを使用することはできません。

上記のように vector aligned ディレクティブを使用すると、ピールループは生成されず、コンパイラーは A と B はすでにアライメントされていると仮定します。

vector aligned ディレクティブを削除すると、コンパイラーは適切なアライメント情報を利用できないため、配列をアライメントするためピールループが生成されます。

カーネルループは、vmovaps と vaddps (アライメントされている) を使用してベクトル化されます。

モジュールに 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 コンパイラー・オプション は、COMMON 内の配列を除く、すべての Fortran 配列に適用されます。このオプションは、配列の開始を n バイト境界でアライメントします。n は、8、16、32、64、128、または 256 のいずれかです。n のデフォルト値は 8 です。配列の要素間にパディングはありません。これは、固定範囲、形状および大きさ引き継ぎ、AUTOMATIC、ALLOCATABLE、POINTER 属性 (ただし、Cray* ポインター配列はアライメント制御がないため除く) に適用されます。

Fortran の配列には、3 つのメモリー形式があります。

  1. 静的メモリー – メモリー内の配列の開始位置は n バイト境界でアライメントされています。
  2. スタックメモリー (ローカル、自動、一時) – スタック内の配列の開始位置は n バイト境界でアライメントされています。
  3. ヒープメモリー (ALLOCATABLE または POINTER 属性、Cray* ポインター配列を除く) – ヒープ内の配列の開始位置が n バイト境界であら面とされるように、aligned_malloc を呼び出す ALLOCATE 文によって割り当てられたメモリーです。

    -align arraynbyte オプションは、コンパイル時にすべての配列をアライメントします。代わりに、個々の配列をアライメントする場合は、個々の配列に ALIGN 属性を指定します。また、プログラマーは、配列へのアクセスがアライメントされている各ループの前に !dir$ vector aligned や !dir$ assume_aligned ディレクティブを追加して、コンパイラーに知らせるべきです。

10. Fortran 2008 CONTIGUOUS 属性

この配列属性は、データが連続するブロックを占有していることをコンパイラーに知らせます。これにより、コンパイラーはポインターと形状引き継ぎ配列を最適化できます。デフォルトでは、コンパイラーはそれらが非連続であると仮定します。しかし、この属性を追加して追加の情報を提供することで、コンパイラーはデータアクセスが連続していると仮定できるようになります。この属性を追加しても実行時にデータが非連続の場合、結果は不定です (正しくない結果、セグメンテーション違反)。

real, pointer, contiguous :: ptr(:)
real, contiguous :: arrayarg(:,:)

POINTER ターゲットと形状引き継ぎ配列に対応する実引数は、連続している必要があります。

また、次の形式の Fortran 2008 組込み関数があります。

is_contiguous() は論理型の戻り値を返します。
IF ( is_contiguous(thisarray) ) THEN
ptr => thisarray

以下は、CONTIGUOUS 属性の使用例です。

subroutine foo1(X, Y, Z, low1, up1)
real, contiguous, intent(inout), dimension(:) :: X
real, contiguous, intent(in), dimension(:) :: Y, Z
integer i, low1, up1

do i = low1, up1
   X(i) = X(i) + Y(i)*Z(i)
enddo

return
end

foo1 のすべての呼び出し位置は 3 つの配列パラメーターの連続するセクションを使用することを示す CONTIGUOUS 属性が追加されています。コンパイラーは、この情報を利用して、ループのストライドの異なる複数バージョンを生成せずに、ユニットストライド形式のベクトルコードのみを生成します。

11. Fortran 配列表記を使用したユニットストライド形式のベクトル化

Fortran 言語セマンティクスは、割付け配列、整合配列、明示配列、大きさ引き継ぎ配列など、さまざまな配列でユニットストライド形式のベクトル化を許可しています。ただし、ベクトルループ・インデックスは最後の次元でなければなりません。そうでない場合は、パフォーマンスを向上するため、F90 配列表記を使用してコードをリファクタリングできる可能性があります。次に例を示します。

オリジナルのソースコード:

do I=1,m
    do index=1,n
         A(I,j,k,index) = B(I,j,k,index) +  C(I,j,k,index) * D
    enddo
enddo
  • 内側のループは、インデックスが最内次元にないため、非ユニットストライド形式でベクトル化されます。
  • ギャザー/スキャッターの効率は低下します。

リファクタリング後のソースコード:

do I=1,m,VLEN
    do index=1,n
         A(I:I+VLEN,j,k,index) = B(I:I+VLEN,j,k,index) + C(I:I+VLEN,j,k,index) * D
    enddo
enddo
  • F90 配列表記を使用することで、内側のループがユニットストライド形式でベクトル化されます。

まとめ

ここでは、Fortran のさまざまな配列型の例と、ローカル変数、および関数/サブルーチン引数としての使用法を説明しました。また、Fortran ポインターの使用法と例も紹介しました。さらに、さまざまな配列データ型と引数が、コンパイラーによってどのようにベクトル化されるか、Fortran の例を使って、コンパイラーにより生成されるコードについて説明しました。

以下は要約です。

  1. 間接配列アクセスは、高コストで非効率なコードにつながります。コンパイラーは、vgather/vscatter を使用して間接的なメモリー参照を含むループをベクトル化できますが、これは効率良いコードではありません。効率良いコードはユニット・ストライド・アクセスです。ポインターや関節参照を使用するコードは、ベクトルコードが生成されるかどうかに関係なく、最適なコードではありません。間接的なメモリー参照を含む場合、コンパイラーは効率良い高速なコードを生成できないことを理解し、可能な限り間接的なメモリー参照を避けます。
  2. 引数渡しに一時配列を使用しないようにします。明示的なインターフェイスを使用して連続する配列データを渡すか、形状引き継ぎ配列を渡します。あるいは、引数を明示的に渡す代わりに、モジュールにデータを配置してモジュールを使用します。
  3. Fortran ポインターは C ポインターよりも制約が多いですが、エイリアスです。ポインターベースの変数の左辺 (LHS) と右辺 (RHS) がオーバーラップする可能性がある場合、LHS に格納する前に RHS 式を評価するため、RHS 式を格納する一時配列が作成されます。可能な限り、ポインターと割付け配列は使用しないようにします。

次のステップ

インテル® Xeon® プロセッサー製品ファミリー上にアプリケーションを移植してチューニングを行うには、次の記事にある各リンクのトピックを参照してください。アプリケーションのパフォーマンスを最大限に引き出すために必要なステップを紹介しています。

ベクトル化の基本」に戻る

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

タイトルとURLをコピーしました