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

同カテゴリーの次の記事

外部ループのベクトル化

この記事は、インテル® デベロッパー・ゾーンに掲載されている「Data Alignment to Assist Vectorization」の日本語参考訳です。


はじめに

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

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

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

  1. データをアライメントします。

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

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

パフォーマンスを向上するには、データをアライメントすることが重要です。また、最適化が行われるように、クリティカル領域でアライメント情報をコンパイラーに知らせることも重要です。データをアライメントしてもコンパイラーに知らせなければ、最適化が十分に行われなかったり、コンパイル時間が長くなる恐れがあります。

アライメントされた静的配列の定義方法 例えば、64 バイト境界でアライメントされた 1000 要素の単精度浮動小数点配列 A を静的に宣言する場合、最適な方法は次のとおりです。
  • Windows* 上で C/C++ を使用する場合:
    __declspec(align(64)) float A[1000];

  • Linux*/Mac* 上で C/C++ を使用する場合:
    float A[1000] __attribute__((aligned(64)));

  • 単純な Fortran 配列の場合: -align arraynbyte (Linux*/Mac OS*)、/align:arraynbyte (Windows*) コンパイラー・オプションを指定するのが最も簡単な方法です。静的または動的のいずれも、array64byte キーワードを指定すると、配列を 64 バイトでアライメントします。このコンパイラー・オプションは、共通ブロックのデータや派生型の要素には影響しません。また、指示句により配列をアライメントすることもできます。コードに指示句を追加することで、変数の宣言時に明示的にアライメントが指定されるため、-align array64byte コンパイラー・オプションを指定する必要はありません。次に指示句の利用例を示します。

    real :: A(1000)
    !dec$ attributes align: 64:: A

  • Fortran の共通データ: -align zcommons (Linux*/Mac OS*)、/align:zcommon (Windows*) コンパイラー・オプションにより、必要に応じてパディングを追加して、すべての共通ブロックの要素を 32 バイト境界でアライメントします。この方法はインテル® MIC には適していませんが、インテル® AVX には最適です。インテル® MIC の場合、完全に 64 バイトでアライメントされる可能性は 2 分の 1 です。
    注: 既存の多くのアプリケーションでは、共通ブロックの要素はパディングなしのパックド値であると仮定しているため、パディングバイトが問題を引き起こす可能性があります。そのため、このコンパイラー・オプションを利用する場合は、必ずアプリケーションの動作検証を行ってください。
  • 共通ブロック: 次のように指示句を利用して開始位置をアライメントすることができます。

          !dec$ attributes align: 64 :: common_name

共通ブロック内の配列はストレージに関連付けられているため、この方法でアライメントすることはできません。インテル® Fortran コンパイラー 13.1 (インテル® Composer XE 2013 Update 2 に含まれる) およびそれ以前のコンパイラーでは、common_name をスラッシュで囲むことはできません。コンパイラーの内部エラーが発生します。将来のバージョンのコンパイラーでは、より明確な次の構文が使用できる予定です:
!dec$ attributes align!: n :: /common_name/

現在、-align array64byte のようなコマンドライン・オプションで、共通ブロックの開始位置をアライメントすることはできません。将来のバージョンのコンパイラーでは、コマンドライン・オプションも利用できるようになる可能性があります。

  • Fortran の派生型データ要素: -align recnbyte コンパイラー・オプションを指定します。インテル® MIC 向けに派生型内のデータ要素を 64 バイト境界でアライメントする場合は、-align rec64byte コンパイラー・オプションを指定します。このコンパイラー・オプションは、派生型のコンポーネントとレコード構造体のフィールドを、設定した境界 (n) でアライメントします。2 番目以降の派生型要素は、メンバーの型のサイズまたは n バイト境界のいずれか小さいほうで格納されます。

  • Fortran のモジュールデータ:

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

    end module mymod

動的データのアライメント

C/C++: malloc() と free() を、アライメントを指定する _mm_malloc() と _mm_free() に置換します。インテル® C++ Composer XE で提供されるこれらの代替関数は、malloc() と free() と同じ引数と戻り型を利用できます。_mm_malloc(n, 64) の戻り値は 64 バイトでアライメントされています。

  • _aligned_malloc()
  • _mm_malloc() および _mm_free()

Fortran: 前述のように、-align arraynbyte および -align recnbyte コンパイラー・オプションを指定します。


2. コンパイラーにアライメント情報を知らせる

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


C/C++ の例: __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 の例: assume_aligned 指示句を指定します。一般的な構文は、次のとおりです。

!dec$ assume_aligned address1:n1 [, address2:n2]…

!

c、C、!、* のいずれか (コンパイラー指示句の構文規則を参照)

address

メモリー参照。任意のデータ型、種別、ランクを指定できます。次のいずれであってはいけません。

  • COMMON 内のエンティティー (または EQUIVALENCE 式により COMMON 内の要素に関連付けられたエンティティー)
  • 派生型の変数のコンポーネントまたはレコードフィールド参照
  • 参照結合またはホスト結合によりアクセスされるエンティティー

n

正の整数初期化式。1 から 256 の範囲の 2 の累乗 (つまり、1、2、4、8、16、32、64、128、256) でなければいけません。

複数の address:n を指定する場合は、各 n をカンマで区切ります。address が Cray* ポインターの場合、または POINTER 属性を持つ場合、アライメントされていると仮定されるのはポインターであって、参照先や TARGET ではありません。n に無効な値を指定すると、エラーメッセージが出力されます。ASSUME_ALIGNED 指示句は、配列の特性を表すものであって、ローカルの使用法を指すものではないため、宣言時に指定するのが最も自然です。モジュール配列の ASSUME_ALIGNED 指示句は、そのモジュールで指定すべきであって、参照結合により配列がアクセスされるループで指定すべきではありません。

!dec$ assume_aligned A: 64 …  do i=1, N A(I) = A(I) + 1 end do

!dec$ assume_aligned A: 64 …
A = A + 1
すべてのメモリー参照がターゲット向けにアライメントされていることをベクトライザーに知らせる方法
(インテル® Composer XE 2011 を使用して) より一般的なプラグマ/指示句をループの前に追加することで、ループ中のすべてのデータがアライメントされていることをコンパイラーに知らせることができます。この方法では、前述のように変数ごとに指定する必要はありません。 C/C++ の例: #pragma vector aligned for (i=0; i<n; i++){     A[i] = B[i] * C[i] + D[i]; } //配列表記文の直前にプラグマを追加して、使用される配列のアライメントを指定します。
#pragma vector aligned A[0:n] = B[0:n] * C[0:n] + D[0:n];
Fortran の例: !dec$ vector aligned do I=1, N A(I) = B(I) * C(I) + D(I) end do

!dec$ vector aligned
A = B * C + D

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

アライメントされているアクセスとされていないアクセスの併用: すべての 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];
}

(icpc でコンパイルされた) C++ プログラムでは、__assume 節が期待どおりに動作しないという既知の問題がありました (C++ のブーリアン型は 8 ビットなので、ベクトライザーは適切に処理できません)。この問題は、インテル® コンパイラー 13.0 Update 1 で修正されています。

以前のバージョンのコンパイラーでは、次のコードを使用しないでください。

__assume(idx_A % 16 == 0);
__assume(idx_B % 16 == 0);
__assume(loc1 % 16 == 0);
__assume(loc2 % 16 == 0);
__assume(stride % 16 == 0);

代わりに、次のコードを使用してください。

__assume_aligned((void*)idx_A, 16);
__assume_aligned((void*)idx_B, 16);
__assume_aligned((void*)loc1, 16);
__assume_aligned((void*)loc2, 16);
__assume_aligned((void*)stride, 16);

まとめ

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

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

次のステップ

この記事は、「Programming and Compiling for Intel® Many Integrated Core Architecture」(英語) の一部「Data Alignment to Assist Vectorization」の翻訳です。インテル® Xeon Phi™ コプロセッサー上にアプリケーションを移植し、チューニングを行うには、各リンクのトピックを参照してください。アプリケーションのパフォーマンスを最大限に引き出すために必要なステップを紹介しています。

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

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

関連記事