一般的なベクトル化のヒント

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

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


この記事は、体系的なステップ・バイ・ステップの最適化フレームワーク手法に従ってコードのパフォーマンスを引き出せるように開発者を支持する、インテル® Modern Code Developer Community (https://software.intel.com/en-us/modern-code) の資料の 1 つです。この記事では、ベクトルレベルの並列化を取り上げます。

ベクトルループ内のユーザー定義の関数呼び出しを制御する

ユーザー定義の関数呼び出しを含むループをベクトル化するには、(コードを再構成して) 関数呼び出しを SIMD 対応関数にします。

SIMD 対応関数内のユニットストライド方式のアクセスを指定する

SIMD 対応関数でユニットストライド方式のメモリーアクセスを行うには、次の 2 つの方法があります。

  • linear 整数でインデックスされた uniform ポインター
  • linear ポインター
__declspec(vector(uniform(a),linear(i:1)))
float foo(float *a, int i){
  return a[i]++;
}

__declspec(vector(linear(a:1)))
float foo1(float *a){
  return (*a)++;
}

ベクトルループ内のメモリーの一義化を制御する

簡単なループをベクトル化する例について考えてみます。

void vectorize(float *a, float *b, float *c, float *d, int n) {
    int i;
    for (i=0; i<n; i++) {
        a[i] = c[i] * d[i];
        b[i] = a[i] + c[i] - d[i];
    }
} 
  • ここで、コンパイラーは 4 つのポインターがどこを指しているか分かりません。プログラマーは 4 つのポインターが別々の場所を指していることを知っていても、コンパイラーは判断できません。4 つのポインターが別々の場所を指していることをプログラマーが明示的にコンパイラーに伝えない限り、コンパイラーはポインターが互いに「非常に不適切に」エイリアスされている (例えば、c[1] と a[0] は同じアドレスであるためループはベクトル化できないなど) と考えます。
  • 不明なポインターの数が「少ない」場合、コンパイラーはランタイムチェックとともにループの最適化されたバージョンと最適化されていないバージョンを生成します (コンパイル時間とコードサイズが増え、ランタイムテストでオーバーヘッドが発生します)。オーバーヘッドは急激に増加するため、この「少ない」数は本当に少ない数 (例えば 2) でなければいけません。また、この場合でも、「ポインターは別々の場所を指している」とコンパイラーに伝えない代償があります。
  • 「ポインターは別々の場所を指している」とコンパイラーに伝える 1 つの方法は、C99 の “restrict” キーワードを利用することです。”C99 標準” でコンパイルしない場合でも、-restrict (Linux*) および /Qrestrict (Windows*) オプションを指定することで、インテル® コンパイラーは “restrict” キーワードを認識することができます。
void vectorize(float *restrict a, float *restrict b, float *c, float *d, int n) {
    int i;
    for (i=0; i<n; i++) {
        a[i] = c[i] * d[i];
        b[i] = a[i] + c[i] - d[i];
    }
}
  • ここでプログラマーは、”a” と “b” がエイリアスされていないことをコンパイラーに伝えます。
  • 別の方法は IVDEP プラグマ (依存性なしと仮定) を利用することです。IVDEP のセマンティクスは restrict ポインターとは異なりますが、コンパイラーが仮定する一部の依存性を排除できるため、コンパイラーはベクトル化が安全であると判断することができます。
void vectorize(float *a, float *b, float *c, float *d, int n) {
    int i;
#pragma ivdep
    for (i=0; i<n; i++) {
        a[i] = c[i] * d[i];
        b[i] = a[i] + c[i] - d[i];
    }
}

64 ビット整数と浮動小数点の変換を避ける

  • 64 ビット整数と浮動小数点を変換しないでください。
  • 代わりに 32 ビット整数を使用してください。
  • 可能であれば、32 ビット符号付き整数を使用してください (最も効率的)。

64 ビット整数でインデックスされたギャザー/スキャッターを避ける

  • 64 ビットでインデックスされたギャザー/スキャッターを効率良くサポートしているハードウェアはありません。

コンパイラーのベクトル化コストモデルに影響を与えるオプション

-vec-threshold[n]: ベクトル化されたループの並列実行が効果的である可能性に基づいて、ループをベクトル化するしきい値を 0 から 100 の間で設定します。デフォルトは 100 です。コストモデルに関係なく、ベクトル化可能なループをすべてベクトル化する場合は、-vec-threshold0 を指定します。

間接アクセスを含むループのベクトル化

間接メモリーロード/ストアを含む次のようなループのベクトル化について考えてみます。

 for (i = kstart; i < kend; ++i) {
    istart = iend;
    iend   = mp_dofStart[i+1];
    float w = xd[i];

    for (j = istart; j < iend; ++j) {
        index  = SCS[j];
        xd[index] -= lower[j]*w;
    }
 }

上記のコードでは、ベクトル化における重要な前提条件は xd 値が明白であることです (xd 値が明白でなければ、ループをベクトル化できない依存性があります。この場合、唯一の代案は、ベクトル化しやすいようにアルゴリズムを書き直すことです)。xd 値が明白であることが分かっていれば、(内部 j ループの前に) ivdep/simd プラグマを指定してベクトル化できます。コンパイラーはまだギャザー/スキャッター命令によるベクトル化を行います。ユニットストライドを利用できる代用アルゴリズム式があれば効果的でしょう。

単調なインダクション変数を含むループ形式のベクトル化

コンパイラーは、単調なインダクション変数 (ループ内のある条件の下でのみ更新されるインダクション変数) を使用する特定のループをベクトル化できます。次に例を示します (このループ形式はコンプレス・イディオム・ループとも呼ばれます)。

    int index_0 = 0;
    for(int k0=0; k0<count0; k0++) {
        TYPE X1 = *(Pos0 + k0);        TYPE Y1 = *(Pos0 + k0 +   count0);
        TYPE Z1 = *(Pos0 + k0 + 2*count0);
        #pragma loop_count min(220) avg (300) max (380)
        #pragma ivdep
        for(int k1=0; k1<count1; k1+=1) {
            TYPE X0 = *(Pos1 + k1);
            TYPE Y0 = *(Pos1 + k1 +   count1);
            TYPE Z0 = *(Pos1 + k1 + 2*count1);
            TYPE diff_X = (X0 - X1);
            TYPE diff_Y = (Y0 - Y1);
            TYPE diff_Z = (Z0 - Z1);
            TYPE norm_2 = (diff_X*diff_X) +  (diff_Y*diff_Y) + (diff_Z*diff_Z);

            if ( (norm_2 >= rmin_2) && (norm_2 <= rmax_2))
                   Packed[index_0++] = norm_2;
        }
    }

変数 index_0 は、特定の条件で更新されます。現在サポートされている節を使ってこのループ形式に SIMD プラグマを使用することは不正ですので注意してください (ivdep プラグマは問題ありません)。

-qopt-assume-safe-padding オプションを追加することで、ループのコード生成シーケンスを高性能にすることができます。このオプションは、変数と動的に割り当てられたメモリーがオブジェクト境界を越えてパディングされているとコンパイラーが仮定するかどうかを制御します。-qopt-assume-safe-padding オプションが指定されると、コンパイラーは変数と動的に割り当てられたメモリーがパディングされていることを仮定します。これは、コードがプログラムで指定されたオブジェクト境界を越えて (最大 64 バイト) アクセスできることを意味します。このオプションを指定すると、コンパイラーは静的および自動オブジェクトにはパディングを追加しませんが、オブジェクトがプログラムにある場合、コードがオブジェクト境界を越えて (最大 64 バイト) アクセスできることを仮定します。この仮定に対応するため、このオプションを指定する場合はプログラムの静的および自動オブジェクトのサイズを増やす必要があります。

コンプレス・イディオム・ループ用に生成されたコードは、このオプションを利用することで高性能になります。次に例を示します。

void foo(float* restrict a, float* restrict b, float* restrict c)
{
   int i;
   int j = 0;
   for(i = 0; i < N; i++) {
       if (b[i])  {
         a[j++] = c[i];
       }
   }
}

-qopt-assume-safe-padding オプションなしのデフォルト設定では、コンパイラーは (メモリーフォルトが発生しないように) 保守的になり、vpackstore と vscatter 命令を使った次のベクトル・コードシーケンスを生成します。

..B1.6:
        vloadunpackld (%rsi,%rax,4), %zmm2
        vprefetch1 512(%rsi,%rax,4)
        vloadunpackld (%rdx,%rax,4), %zmm3
        vprefetch0 256(%rsi,%rax,4)
        vloadunpackhd 64(%rsi,%rax,4), %zmm2
        vprefetch1 512(%rdx,%rax,4)
        vloadunpackhd 64(%rdx,%rax,4), %zmm3
        vprefetch0 256(%rdx,%rax,4)
        vcmpneqps %zmm1, %zmm2, %k1
        movl      $65535, %r10d
        vpackstorelps %zmm3, -64(%rsp){%k1}
        lea       (%rdi,%r8,4), %r11
        vmovaps   -64(%rsp), %zmm4
        kmov      %k1, %r9d
        popcnt    %r9d, %ecx
        addq      $16, %rax
        movl      %ecx, %ecx
        shll      %cl, %r10d
        addq      %rcx, %r8
        xorl      $-1, %r10d
        kmov      %r10d, %k2
..L7:
        vscatterdps %zmm4, (%r11,%zmm0,4){%k2}
        jkzd      ..L6, %k2
        vscatterdps %zmm4, (%r11,%zmm0,4){%k2}
        jknzd     ..L7, %k2
..L6:
        cmpq      $992, %rax
        jb        ..B1.6

-qopt-assume-safe-padding オプションを追加すると (この例では、ユーザーが配列を割り当てるときに配列に 64 バイトのパディングを追加すると安全に処理できます)、コンパイラーは、より高性能な次のコードを生成します。

..B1.6:
        vloadunpackld (%rsi,%rax,4), %zmm1
        vprefetch1 512(%rsi,%rax,4)
        vloadunpackld (%rdx,%rax,4), %zmm2
        vprefetch0 256(%rsi,%rax,4)
        vloadunpackhd 64(%rsi,%rax,4), %zmm1
        vprefetch1 512(%rdx,%rax,4)
        vloadunpackhd 64(%rdx,%rax,4), %zmm2
        vprefetch0 256(%rdx,%rax,4)
        vcmpneqps %zmm0, %zmm1, %k1
        movl      $65535, %r10d
        vpackstorelps %zmm2, -64(%rsp){%k1}
        addq      $16, %rax
        vmovaps   -64(%rsp), %zmm3
        kmov      %k1, %r9d
        popcnt    %r9d, %ecx
        movl      %ecx, %ecx
        shll      %cl, %r10d
        xorl      $-1, %r10d
        kmov      %r10d, %k2
        vpackstorelps %zmm3, (%rdi,%r8,4){%k2}
        vmovaps   -64(%rsp), %zmm4
        nop
        vpackstorehps %zmm4, 64(%rdi,%r8,4){%k2}
        addq      %rcx, %r8
        cmpq      $992, %rax
        jb        ..B1.6

次のステップ

インテル® アドバンスト・ベクトル・エクステンション (インテル® AVX) アーキテクチャー上にアプリケーションを移植してチューニングを行うには、各リンクのトピックを参照してください。アプリケーションのパフォーマンスを最大限に引き出すために必要なステップを紹介しています。

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

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

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