ベクトル化の可能性を高めるデータ・アライメント

HPCインテル® DPC++/C++ コンパイラーインテル® Fortran コンパイラー

この記事は、インテル® デベロッパー・ゾーンに掲載されている「Data Alignment to Assist Vectorization」(http://software.intel.com/en-us/articles/data-alignment-to-assist-vectorization) の日本語参考訳です。


はじめに

データ・アライメントとは、特定のバイト境界上のメモリーにデータ・オブジェクトを生成するようにコンパイラーに指示する手法です。これは、データのロード/ストアの効率を高めるために行います。簡単に説明すると、プロセッサーの設計特性により、データが特定のバイト境界上のメモリーアドレスにある場合、そのデータを効率良く移動することができます。インテル® メニー・インテグレーテッド・コア (インテル® MIC) アーキテクチャーでは、データが 64 バイト境界で始まるアドレスにあると、メモリーの移動を最適に行えます。そのため、モジュロ 64 バイトで始まるアドレス境界にデータ・オブジェクトを生成するようにコンパイラーに指示すると良いでしょう (そうすることで、ベースポインターがアライメントされます)。

さらに、データアクセス (ベースポインターとインデックスを含む) が 64 バイトでアライメントされていることが判明している場合、コンパイラーはさまざまな最適化を適用できます。デフォルトでは、ユーザーのヒントなしでは、コンパイラーはループ内のデータがアライメントされているか情報を得ることも、仮定することもできません。そのため、インテル® コンパイラーが最適なコードを生成できるように、プラグマ (C/C++)/ディレクティブ (Fortran)、オプション (Fortran の -aliagn array64byte など)、および節/属性の組み合わせによって、コンパイラーにアライメント情報を知らせる必要があります。

データをアライメントするには、次の 2 つのステップに従います。

  1. データをアライメントします。
  2. クリティカル領域内のデータが使用されている位置で、プラグマ/ディレクティブを記述してメモリーアクセスがアライメントされていることをコンパイラーに知らせます。

1. データをアライメントする

アプリケーション・パフォーマンスを向上するためデータをアライメントすることが重要です。通常、これは 2 つのことを意味します。

  • 配列 (またはポインター) に割り当てられる空間のベースポインターのアライメント
  • 開始インデックスに各ベクトルループ (各スレッド) にとって有益なアライメント属性が指定されていること

STATIC 配列 (ベースポインター) のアライメント

静的配列では、以下に示す適切な節/属性またはオプションを使用してベースポインターをアライメントします。そうすることで、データが適切な境界に割り当てられ、コンパイラーはすべての使用位置でこれらの配列のベースポインターのアライメントを認識します。

Fortran では、以下に示すように、-align array64byte オプションを指定してコンパイルすると、これらの属性は動的配列にも拡張されます。同様に、Fortran モジュールデータ (割付け配列を含む) のアライメントに !dir$ attributes align:64 を使用すると、コンパイラーは USE 位置でベースポインターのアライメントを検出します。

例: 1000 要素の単精度浮動小数点配列 A を (インテル® MIC アーキテクチャーに最適な) 64 バイト境界で静的に宣言します。

  • Windows* の C/C++
    __declspec(align(64)) float A[1000];
    
  • Linux*/macOS* の C/C++
    float A[1000] __attribute__((aligned(64)));
    
  • Fortran の単純な配列: インテル® コンパイラーの -align arraynbyte オプションを使用します。Fortran で配列データをアライメントする最も簡単な方法は、-align array64byte コンパイラー・オプションですべての配列 (静的と動的) を 64 バイト境界でアライメントすることです。このオプションは、COMMON ブロック内のデータと派生型内の要素はアライメントしません。また、ディレクティブにより配列をアライメントすることもできます。コードにディレクティブを追加することで、変数の宣言時に明示的にアライメントが指定されるため、-align array64byte コンパイラー・オプションを指定する必要はありません。次に例を示します。
    real :: A(1000)
    !dir$ attributes align: 64:: A
    
  • Fortran の COMMON データ: パディングを追加するか (additional variables)、必要に応じて、データ: パディング (追加の変数) を追加するか、配列の次元を増やすことで、すべての COMMON ブロック・エンティティーを 64 バイト境界でアライメントします。-align zcommons などのオプションは、ベクトル化のためデータをアライメントしません。-align zcommons は、32 バイトまたは自然なアライメントのいずれか小さいほうにデータをパディングします。データが REAL(8) 配列の場合、32 バイト境界ではなく、8 バイト境界でアライメントされます。

    注: Fortran 標準に違反するだけでなく、パディングバイトの自動挿入は、COMMON エンティティーはパックされており、パディングを含んでいないと仮定する多くのレガシー・アプリケーションで不具合を引き起こす可能性があります。自動パディングは通常よいアイデアとは言えません。代わりに、自然なアライメントを保証するため、COMMON ブロック内の変数を順序付けして、最も大きなデータ型が最初に表れるように、または異なるサイズのデータごとに個別の COMMON ブロックを作成します。

  • 共通ブロック: 次のようにディレクティブを利用して開始位置をアライメントすることができます。
    !DIR$ ATTRIBUTES ALIGN : 64 :: /common_name/
    

共通ブロック内の配列はストレージに関連付けられているため、この方法でアライメントすることはできません。COMMON ブロックの開始は、現在 -align array64byte などのコマンドライン・オプションを使用してアライメントできません。将来のバージョンのコンパイラーでは、コマンドライン・オプションも利用できるようになる可能性があります。

  • Fortran 派生型データ要素: 派生型のコンポーネントの割付け配列は、動的データの場合、次に示すようにアライメントできます。静的データの場合、この方法でアライメントすることはできません。-align recnbyte コンパイラー・オプションは、一般に、レコード構造内の派生型および派生フィールドのコンポーネントを指定された境界 (n) でアライメントしません。「Fortran の COMMON データ」で示すように、単に自然なアライメントを保証するだけです。派生型を SEQUENCE にすると、前述のように、パディング変数を挿入するか、配列の次元を増やすことでコンポーネントのアライメントを保証できます。また、COMMON ブロックでは、ATTRIBUTES ALIGN ディレクティブを使用して派生型の開始をアライメントできます。
  • Fortran のモジュールデータ: 次のコード例を参照してください。
    module mymod
    real, allocatable :: a(:), b(:)
    real              :: c(1000)
    !dir$ attributes align:64 :: a
    !dir$ attributes align:64 :: b
    !dir$ attributes align:64 :: c
    ...
    end module mymod
    

-align array64byte オプションは、静的配列を含むモジュール内の配列にも適用されます。

DYNAMIC データのアライメント

C/C++: malloc()free() をアライメントが指定された _mm_malloc()_mm_free() に置き換えます。これらのルーチンは、第 2 引数としてアライメント・パラメーター (バイト単位) を受け取ります。インテル® C++ コンパイラーによって提供されるこれらの置換も、malloc() および free() と同じサイズ引数を使用し、同じ型を返します。_mm_malloc(p, 64) によって返されるデータは 64 バイトでアライメントされています。

  • _aligned_malloc()
  • _mm_malloc() および _mm_free()
  • buf = (char*) _mm_malloc(bufsizes[i], 64);

動的に割り当てられる C/C++ 配列では、作成時に mm_malloc でアライメントすることは必須ですが、これだけでは不十分であり、対象のループの前で __assume_aligned(a, 64) 節を指定する必要があります。この節を指定しないと、コンパイラーは配列へアクセスする最適なアライメントを検出しません。詳細は、後述します。

Fortran: Fortran モジュールの割付け配列のアライメントには、-align array64byte コンパイラー・オプションまたは !dir$ attributes align:64 を使用します。「Fortran の配列データおよび引数とベクトル化」のアライメント関連のセクションも参照してください。

ループ内のデータの開始インデックスのアライメント属性の設定

a[i+n1] 形式でメモリーにアクセスする i ループでは、開始インデックスが特定のアライメント属性を持つようにループを構成する必要があります。つまり、(i ループの下限 + n1) が 16 の倍数 (float データ型を仮定) になるように保証する必要があります。

また、コンパイル時にこの情報が利用可能な場合を除き、この属性についてコンパイラーに知らせる必要があります (例: x[i] 形式でアクセスし、ループの下限はすべてのスレッドで定数 0 の場合、ループ内では b[i+16*k] 形式でアクセスします)。その他すべてのケースでは、ループの前に __assume(n1%16==0) 節または #pragma vector aligned も追加する必要があります。詳細は、次のセクションで説明します。

2. データ・アライメントについてコンパイラーに知らせる

データをアライメントしたら、プログラム中の実際にデータが使われる場所で、データがアライメントされていることをコンパイラーに知らせる必要があります。例えば、パフォーマンス・クリティカルな関数やサブルーチンにデータを引数として渡す場合、コンパイラーにはそのデータがアライメントされているかどうかが分かりません。コンパイラーは引数に関する情報を持っていないため、この情報はユーザーが提供する必要があります。

i ループ内の (float 配列の) メモリーアクセス (a[i+n1]X[i] など) に対して、 コンパイラーがアライメントされたロード/ストアを生成するには、次の情報が必要です。

  1. ベースポインター (a と X) がアライメントされていること。静的配列では、__declspec(align(64)) を使用するなど、前述の手法を使用してこれを達成できます。動的に割り当てられる配列では、作成時に mm_malloc アライメントするだけでは十分ではなく、次に示すように __assume_aligned(a, 64) 節が必要です。
  2. (i ループの下限 + n1) が 16 の倍数であること。(このループを実行するすべてのスレッドで) 下限が 0 の場合、n1 が 16 の倍数であるという情報が必要です。コンパイラ―にこれを知らせる 1 つの方法として、__assume(n1%16==0) 節を追加します。

__assume_aligned__assume などの節は、節が指定されたプログラム内の特定の場所でその属性が保持されることをコンパイラ―に知らせます。つまり、__assume_aligned(a, 64); は、プログラムの実行がこのポイントに到達すると、ポインター a は常に 64 バイトでアライメントされていることを示します。コンパイラーは、この属性をプログラム内のほかのポイント (後続のループなど) に伝播する場合がありますが、この動作は保証されません (コンパイラーは保守的に仮定する必要があり、同じ関数の後続のループに属性を安全に適用できないかもしれません)。

これらの節は、ループの直前に指定され、そのループのみに適用される #pragma vector aligned などのループプラグマ (またはディレクティブ) とは異なります。例えば、__assume_aligned および __assume 節は、ループ内部でも指定することができますが、プラグマはループの前に追加する必要があります。

C/C++ の例 1:

__declspec(align(64)) float X[1000], X2[1000];

void foo(float * restrict a, int n, int n1, int n2) {
  int i;
  __assume_aligned(a, 64);
  __assume(n1%16==0);
  __assume(n2%16==0);

  for(i=0;i<n;i++) { // Compiler vectorizes loop with all aligned accesses
    X[i] += a[i] + a[i+n1] + a[i-n1]+ a[i+n2] + a[i-n2];
  }

  for(i=0;i<n;i++) { // Compiler vectorizes loop with all aligned accesses
    X2[i] += X[i]*a[i];
  }
}

コンパイラーにより生成されたアライメントされたアクセスが、ベクトル化されたループで想定どおりに動作するか確認することは、常によいことです。この情報は、コンパイラーにより出力される最適化レポートに含まれています。例えば、上記のコードをコンパイルすると、次のメッセージが出力されます。

% icc -O3 -qopt-report-phase=vec -qopt-report=5 -qopt-report-file=stdout -restrict -c test1.c
Begin optimization report for: foo(float *, int, int, int)

    Report from: Vector optimizations [vec]


LOOP BEGIN at test1.c(11,3)
   remark #15388: vectorization support: reference X has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference X has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(12,5) ]
   remark #15305: vectorization support: vector length 4
   remark #15399: vectorization support: unroll factor set to 2
   remark #15300: LOOP WAS VECTORIZED
   remark #15448: unmasked aligned unit stride loads: 6
   remark #15449: unmasked aligned unit stride stores: 1
   remark #15475: --- begin vector loop cost summary ---
   remark #15476: scalar loop cost: 28
   remark #15477: vector loop cost: 3.250
   remark #15478: estimated potential speedup: 8.610
   remark #15488: --- end vector loop cost summary ---
LOOP END

LOOP BEGIN at test1.c(11,3)
<Remainder loop for vectorization>
   remark #15388: vectorization support: reference X has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference X has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(12,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(12,5) ]
   remark #15305: vectorization support: vector length 4
   remark #15309: vectorization support: normalized vectorization overhead 0.370
   remark #15301: REMAINDER LOOP WAS VECTORIZED
LOOP END

LOOP BEGIN at test1.c(11,3)
<Remainder loop for vectorization>
LOOP END

LOOP BEGIN at test1.c(15,3)
   remark #15388: vectorization support: reference X2 has aligned access   [ test1.c(16,5) ]
   remark #15388: vectorization support: reference X2 has aligned access   [ test1.c(16,5) ]
   remark #15388: vectorization support: reference X has aligned access   [ test1.c(16,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(16,5) ]
   remark #15305: vectorization support: vector length 4
   remark #15399: vectorization support: unroll factor set to 4
   remark #15300: LOOP WAS VECTORIZED
   remark #15448: unmasked aligned unit stride loads: 3
   remark #15449: unmasked aligned unit stride stores: 1
   remark #15475: --- begin vector loop cost summary ---
   remark #15476: scalar loop cost: 10
   remark #15477: vector loop cost: 2.000
   remark #15478: estimated potential speedup: 4.840
   remark #15488: --- end vector loop cost summary ---
LOOP END

LOOP BEGIN at test1.c(15,3)
<Remainder loop for vectorization>
   remark #15388: vectorization support: reference X2 has aligned access   [ test1.c(16,5) ]
   remark #15388: vectorization support: reference X2 has aligned access   [ test1.c(16,5) ]
   remark #15388: vectorization support: reference X has aligned access   [ test1.c(16,5) ]
   remark #15388: vectorization support: reference a has aligned access   [ test1.c(16,5) ]
   remark #15305: vectorization support: vector length 4
   remark #15309: vectorization support: normalized vectorization overhead 0.625
   remark #15301: REMAINDER LOOP WAS VECTORIZED
LOOP END

LOOP BEGIN at test1.c(15,3)
<Remainder loop for vectorization>
LOOP END
===========================================================================

コアループがベクトル化され、2 でアンロールされ、ソース内の両方のループへのアクセスがすべてアライメントされてたことが分かります。

C/C++ の例 2: 特定 の変数では、__assume_aligned マクロを使用して特定の変数または引数がアライメントされていることをコンパイラーに知らせます。例えば、引数として渡される配列やグローバル配列がアライメントされていることをコンパイラーに知らせるには、次のように記述します。

void myfunc( double p[] )
{
   __assume_aligned(p, 64);
   for (int i=0; i<n; i++)
   {
      p[i]++;
   }
}

void myfunc2( double *p2, double *p3, double *p4, int n) 
{
   for (int j=0; j<n; j+=8) {
      __assume_aligned(p2, 64);
      __assume_aligned(p3, 64);
      __assume_aligned(p4, 64);
      p2[j:8] = p3[j:8] * p4[j:8];
   }
}

Fortran の 1: ASSUME_ALIGNED ディレクティブを使用します。ASSUME_ALIGNED は、配列の特性を表すものであって、ローカルの使用法を指すものではないため、宣言時に指定するのが最も自然です。モジュール配列の ASSUME_ALIGNED ディレクティブは、そのモジュールで指定すべきであって、参照結合により配列がアクセスされるループで指定すべきではありません。

!DIR$ ASSUME_ALIGNED A: 64
... 
do i=1, N
A(I) = A(I) + 1
end do

!DIR$ ASSUME_ALIGNED A: 64
...
A = A + 1

Fortran の 2: ASSUME ディレクティブを使用してアライメント属性を指定します。この例では、コンパイラーはユーザー指定のディレクティブを利用して、ループ内のすべてのアクセスがアライメントされたコードを生成します。ここで、ASSUME は f2 の長さが 8 の倍数であることを示します。

subroutine sot(a,f2,st,rep,rstr)
real*8 :: a(*),a1
integer f2,st,rep,rstr,i
!DIR$ ASSUME_ALIGNED A: 64
!DIR$ ASSUME (mod(f2,8) .eq. 0)
!dir$ simd
do i=1,(rep-1)*rstr+1
  a1=a(i)+a(f2*st+i)
  a(f2*st+i)=a(i)-a(f2*st+i)
  a(i)=a1
enddo
end subroutine

以下の例は、ベクトルプラグマ/ディレクティブを使用してベクトライザーをオーバライドする方法を示します。

C/C++ の例 3: これは、アライメントされたアクセスとされていないアクセスを管理する手法です。__assume_aligned__assume を使用して、すべての RHS メモリー参照が 64 バイトでアライメントされていることをコンパイラ―に知らせます。

float p, p1;
__assume_aligned(p,64);
__assume_aligned(p1,64);
__assume(n1%16==0);
__assume(n2%16==0);
__assume(n3%16==0);
__assume(n4%16==0);
__assume(n5%16==0);

for(i=0;i<n;i++){
   q[i] = p[i]+p[i+n1]+p[i+n2]+p[i+n3]+p[i+n4]+p[i+n5];
}
for(i=0;i<n;i++){
   q1[i] = p1[i]+p1[i+n1]+p1[i+n2]+p1[i+n3]+p1[i+n4]+p1[i+n5];
}

VECTOR プラグマ/ディレクティブの使用

より一般的なプラグマ/ディレクティブをループの前に追加して、ループ内のすべてのデータがアライメントされていることをコンパイラーに知らせることができます。この方法では、前述のように変数ごとに指定する必要はありません。ループの前に #pragma vector aligned を追加して、ループ内のすべての配列アクセスがアライメントされていることを示します (アライメントされていることを保証するのはプログラマーの責任です。アライメントされていない場合、セグメンテーション違反になります)。このプラグマ/ディレクティブは、対象となるベクトルループの直前に追加する必要があります。

C/C++ の例 4:

__declspec(align(64)) float X[1000], X2[1000];
void foo(float * restrict a, int n, int n1, int n2) {
  int i;
#pragma vector aligned  
  for(i=0;i<n;i++) { // Compiler vectorizes loop with all aligned accesses
    X[i] += a[i] + a[i+n1] + a[i-n1]+ a[i+n2] + a[i-n2];
  }

#pragma vector aligned
  for(i=0;i<n;i++) { // Compiler vectorizes loop with all aligned accesses
    X2[i] += X[i]*a[i];
  }
}

Fortran の例 3:

!DIR$ VECTOR ALIGNED
do I=1, N
A(I) = B(I) * C(I) + D(I)
end do

!DIR$ VECTOR ALIGNED
A = B * C + D

注: これらの節は、コンパイラーのベクトライザーの効率ヒューリスティックを直接オーバーライドします。すべての配列参照に対して、アライメントされたデータ移動命令を利用するようにコンパイラーに指示します。プログラムのコンテキストからアライメント属性を判断したり、動的ループピーリングにより参照をアライメントするなど、コンパイラーによるアライメントの高度な最適化はすべて無効になります。これらの節は注意して指定してください。また、アライメントされたデータ移動命令を利用してすべての配列参照を実装すると、アライメントされていないアクセスパターンがあった場合にランタイム例外が発生します。

注: OpenMP* 4.0 標準には、ALIGNED 節を指定可能な !$OMP SIMD ディレクティブがあります。これは、ベクトル化するが、スレッド化しないループ内のデータのアライメントを指定するのにも使用できます。

ベクトルのアライメントとループの並列化

OpenMP* 並列ループ (外側のレベル) 内にベクトル化されたループ (内側レベル) が入れ子されている場合、アライメント・プラグマ/ディレクティブはシーケンシャルの場合と同様に動作します。同じ手法を使用して、ベクトルプラグマ/ディレクティブでループをマークして、コンパイラーがアライメントされたアクセスでベクトル化されたコードを生成するようにできます。

Fortran の例 4:

subroutine initialize(a,b,c)
  double precision a(ni,nj,nk), b(ni,nj,nk), c(ni,nj,nk)

  integer i, j, k

!$OMP PARALLEL do  collapse(2)
    do k=1, nk
       do j=1, nj
!dir$ vector aligned
         do i=1, ni
            a(i,j,k) = 2.d0*i
            b(i,j,k) = 3.d0*j
            c(i,j,k) = 4.d0*k
         end do
      end do
    end do
end subroutine initialize

(入れ子のループではなく) 同じレベルのループで並列化とベクトル化を行う場合、追加の考慮事項があります。この場合、ループの下限が適切 (例えば、C では 0、Fortran では 1) であっても、ワークを分割後、各スレッドはそれぞれのループの下限で開始します。これらの下限は、OpenMP* のスケジュール・タイプ (static-chunk、static-even、dynamic、guided など) やスレッド数に基づいて実行時に決定され、コンパイラーはこれらの値を知りません。ループに #pragma vector aligned を追加する場合、各スレッドが最適なアライメント属性を持つようにランタイム・パラメーターを制限するのはユーザーの責任です。そうでない場合、アプリケーションは一部の OpenMP* ランタイム・パラメーターでセグメント違反になります。

OpenMP* のスタティック・スケジューリングの調整が適切に行われるようにする 1 つの方法として、次に示すように、並列ループの数を制限して、残りのループがシリアルに処理されるようにします。

C/C++ の例 5a: 目的のアライメントを達成できるように次のループを書き直ります。

   static double * a;
   a = _mm_malloc((N+OFFSET) * sizeof(double),64); 
   #pragma omp parallel for
   for (j = 0; j < N; j++)
      a[j] = 2.0E0 * a[j];

C/C++ の例 5b:

    static double * a;
    a = _mm_malloc((N+OFFSET) * sizeof(double),64);
    #pragma omp parallel
    {
        #pragma omp master
        {
           num_threads = omp_get_num_threads();
           // Assuming omp static scheduling, carefully limit the loop-size to N1 instead of N 
           N1 = ((N / num_threads)/8) * num_threads * 8;
          // printf("num_threads = %d, N = %d, N1 = %d, num-iters in remainder 
          // serial loop = %d, parallel-pct = %f\n", num_threads, 
          // N, N1, N-N1, (float)N1*100.0/N);
        }
    }
  
    #pragma omp parallel for
    #pragma vector aligned
    for (j = 0; j < N1; j++) // Compiler vectorizes loop with all aligned accesses
        a[j] = 2.0E0 * a[j];

    // Serial loop to process the last N-N1 iterations
    for (j = N1; j < N; j++)
        a[j] = 2.0E0 * a[j];

C/C++ の align_value 属性

インテル® C/C++ コンパイラー 14.0 以降は、次に示すように align_value 属性をサポートします。

  • Windows*:
    __declspec(align_value(alignment))
    
  • Linux*:
    _attribute__((align_value(alignment)))
    

この属性の引数は、ポインターの指示先のアライメント (8、16、32、64、… バイト) を指定します。このキーワードをポインターの型宣言に追加して、そのポインター型のポインターのアライメント値を指定できます。

ポインター型を使用して仮引数を宣言すると、コンパイラーは関数への入り口で仮引数に __assume_aligned ディレクティブを挿入します。ポインター型を使用してローカル変数を宣言すると、コンパイラーはローカルポインターの初期化位置に __assume_aligned ディレクティブを挿入します。グローバル宣言には、ディレクティブは挿入されません。

注: この型定義で変数が宣言されたら、ソースコード内のその変数のすべてのインスタンスが適切なアライメント属性になるように保証するのはユーザーの責任です。例えば、以下の例の *(out1++) 形式のポインター演算は、ポインター演算後に 64 バイトのアライメントが失われるため、使用できません。

C/C++ の例 6:

typedef double* __restrict__  __attribute__((align_value (64))) Real_ptr;

void T1(int len, Real_ptr out1, Real_ptr out2, Real_ptr out3, Real_ptr in1, Real_ptr in2)
{
   for (int i=0; i<len ; ++i) { // Compiler vectorizes loop with all aligned acesses
      out1[i] = in1[i] * in2[i] ;
      out2[i] = in1[i] + in2[i] ;
      out3[i] = in1[i] - in2[i] ;
   }
}

C/C++ の例 7: この例は、align_value 属性で宣言されたローカル変数の使用法を示します。

typedef double* __restrict__  __attribute__((align_value (64))) Real_ptr;
typedef int Indx_type;

struct LoopData {
   double *in1 ;
   double *in2 ;
   double *out1 ;
   double *out2 ;
   double *out3 ;
} ;

void foo3(LoopData& d, Indx_type len)
{
   Real_ptr local_in1  = d.in1;
   Real_ptr local_in2  = d.in2;
   Real_ptr local_out1 = d.out1;
   Real_ptr local_out2 = d.out2;
   Real_ptr local_out3 = d.out3;

   for(Indx_type i=0; i<len; ++i) {
      local_out1[i] = local_in1[i] * local_in2[i] ;
      local_out2[i] = local_in1[i] + local_in2[i] ;
      local_out3[i] = local_in1[i] - local_in2[i] ;
   }
}

C/C++ の例 8: この例は、ラムダ関数で align_value 属性を使用する方法を示します。

typedef double* __restrict__  __attribute__((align_value (64))) Real_ptr;

template <typename LOOP_BODY>
inline  __attribute__((always_inline))
void forall(int begin, int end, LOOP_BODY loop_body)
{
   for ( int ii = begin ; ii < end ; ++ii ) {
      loop_body( ii );
   }
}

void foo2(int len, Real_ptr out1, Real_ptr out2, Real_ptr out3, Real_ptr in1, Real_ptr in2)
{
   forall(0, len, [&] (Indx_type i) { // Compiler vectorizes loop with all aligned accesses
      out1[i] = in1[i] * in2[i] ;
      out2[i] = in1[i] + in2[i] ;
      out3[i] = in1[i] - in2[i] ;
   } ) ;
}

まとめ

データ・アライメントとは、特定のバイト境界上のメモリーにデータ・オブジェクトを生成するようにコンパイラーに指示する手法です。これは、データのロード/ストアの効率を高めるために行います。インテル® メニー・インテグレーテッド・コア (インテル® MIC) アーキテクチャーでは、データが 64 バイト境界で始まるアドレスにあると、メモリーの移動を最適に行えます。

さらに、データが 64 バイトでアライメントされていることが判明している場合、コンパイラーはさまざまな最適化を適用できます。そのため、コンパイラーが最適なコードを生成できるように、プラグマ (C/C++)/指示句 (Fortran) によってコンパイラーにアライメント情報を知らせる必要があります。ただし、例外として Fortran のモジュールデータは USE 文でアライメント情報を受け取ります。

次のステップ

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

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

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

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