インテル® C++ コンパイラー 18.0 デベロッパー・ガイドおよびリファレンス

インテル® グラフィックス・テクノロジーにおけるベクトル化

このトピックは、インテル® グラフィックス・テクノロジーをターゲットとする場合にのみ適用されます。

プロセッサー・グラフィックスのパフォーマンスを引き出すには、ベクトル化が不可欠です。コンパイラーは、プログラミングの労力 (特にインテル® Cilk™ Plus 言語拡張のサブセット) が最小限になる効率良いコードを生成します。インテル® Cilk™ Plus は、インテル® C++ コンパイラー 18.0 では非推奨の古い機能です。プロセッサー・グラフィックスへオフロードする代替手段は、将来のリリースで提供される予定です。詳細は、「インテル® Cilk™ Plus の代わりに OpenMP* またはインテル® TBB を使用するためのアプリケーションの移行」を参照してください。

コンパイラーのベクトル化に任せることもできますが、#pragma simd や配列表記のような明示的なベクトル化を使用することを推奨します。

#pragma simd は、ループをベクトル化するようにコンパイラーに指示します。ループをベクトル化できない場合、コンパイラーは警告を出力します。

#pragma simd で外側のループのベクトル化がサポートされました。ループは垂直にベクトル化され、複数の入れ子のループを含めることができます。ベクトル化されたループには、構造体と配列へのアクセスやベクトル関数の呼び出しを含む、多様なコーディング・パターンを含めることができます。

ベクトル関数または SIMD 対応関数は外側のループのベクトル化に大きな影響を与えます。ベクトル関数は、ループの単一の反復に対するスカラー関数として、あるいはベクトル化されたループの複数の反復に対して並列に呼び出すことができる関数です。SIMD 対応関数を定義するには、__declspec(vector) (Windows* および Linux*) または __attribute__((vector)) (Linux* のみ) でアノテーションを付けて、関数のスカラー形式とベクトル形式の両方を生成するようにコンパイラーに指示します。ベクトル形式では、__declspec(vector)/__attribute__((vector))linear または uniform のいずれかが指定されない限り、関数の引数はすべてベクトル化されます。linearuniform は呼び出し元と SIMD 対応関数のベクトル形式との間の特定の変換を示し、どちらも最適化に使用されます。値のベクトルの代わりに単一のスカラー値を渡すことができます。

配列表記は、コンパクトなデータ並列コードの記述を可能にする強力な拡張機能です。ベクトル化が有効な場合、コンパイラーはベクトルコードを使用して配列表記を実装します。

配列表記の基本的な形式は、添字演算子に似ていますが、操作が単一の要素ではなく配列の部分に適用されるセクション演算子です。

section_operator ::= [ lower_bound : length : stride ]

lower bound、length、stride は整数型で、次に示すように整数値のセットを表します。

lower_bound, (lower_bound + stride), …, lower_bound + (length - 1) * stride

例えば、A[2:8:2] は、配列 A の 8 つの要素 (インデックス 2、4、...、16) を指します。

配列表記は、暗黙のインデックス変数とリダクション関数へのアクセスのような機能も提供します。

Crossfade の例

下記のコードは、2 つのイメージをマージする非常に単純なイメージフィルターです。平凡なスカラーコードに 2 つのプラグマを追加しています。

bool CrossFade::execute_offload (int do_offload)
{  
   //ここでは、次の理由から、オフロードで使用される "this" オブジェクト・
   // メンバーの一時コピーを作成します。
   // - ポインター型のメンバー: オフロード境界でポインターのマーシャリングは
   // サポートされていないため、正しく動作しません。
   // ターゲットは CPU 形式のポインター値をどう扱うべきか判断できません。
   // - すべてのオブジェクト・メンバー: 効率が悪いため、ターゲットで
   // 二重の間接参照は行うべきではありません。
   unsigned char * inputArray1 = m_inputArray1, * inputArray2 = m_inputArray2,
   * outputArray = m_outputArray;
   int arrayWidth = m_arrayWidth, arrayHeight = m_arrayHeight, arraySize = m_arraySize;

   unsigned a1 = 256 - m_blendFactor;
   unsigned a2 = m_blendFactor;

   // offload プラグマは、共有 (またはコピー) されるデータをすべて
   // リストします。offload プラグマには必須の並列ループが続きます。
   // これは _Cilk_for または配列表記文で表現され、後続のループが並列ループであり、
   // ターゲットで並列化される必要があることを示します。
   #pragma offload target(gfx) if (do_offload) \
     pin(inputArray1, inputArray2, outputArray: length(arraySize))
   _Cilk_for (int i=0; i<arraySize; i++){
     outputArray[i] = (inputArray1[i] * a1 + inputArray2[i] * a2) >> 8;
   }
     return true;
}

タイルを使用した行列乗算の例

このコードは、データ並列を簡潔に表現するため明示的なタイルと配列表記を使用した行列乗算コードです。

bool MatmultLocalsAN::execute_offload(int do_offload)
{
      // 上の例と同様に、"this" オブジェクト・メンバーの一時コピーを
      // 作成します。
      int m = m_height, n = m_width, k = m_common;
      float (* A)[k] = (float (*)[])m_matA;
      float (* B)[n] = (float (*)[])m_matB;
      float (* C)[n] = (float (*)[])m_matC;

      // A、B、C は配列へのポインターですが、長さはポイント先の
      // 配列のサイズ単位ではなく、ポイント先の配列の要素で
      // 指定されます。
      #pragma offload target(gfx) if (do_offload) \
            pin(A: length(m*k)), pin(B: length(k*n)), pin(C: length(m*n))
      // 完全な入れ子構造の並列ループはコンパイラーにより結合できます。
      _Cilk_for (int r = 0; r < m; r += TILE_m) {
         _Cilk_for (int c = 0; c < n; c += TILE_n) {
         // これらの配列はインテル® グラフィックス・
         // テクノロジー・レジスター・ファイル (GRF) で割り当てられ、
         // 非常に効率良いコードになります。
         float atile[TILE_m][TILE_k], btile[TILE_n], ctile[TILE_m][TILE_n];
         // ctile を初期化する配列表記構文。
         // GRF へ直接アクセスできるようにアンロールする
         // 一連のベクトル操作を行います。
         #pragma unroll
         ctile[:][:] = 0.0f;
         for (int t = 0; t < k; t += TILE_k) {
            // 一連のベクトルのロードを行います。
            #pragma unroll
            atile[:][:] = A[r:TILE_m][t:TILE_k];
            // GRF へ直接アクセスできるようにアンロールします。
            #pragma unroll
 
            for (int rc = 0; rc < TILE_k; rc++) {
               // ベクトルのロードを行います。
               btile[:] = B[t+rc][c:TILE_n];
               #pragma unroll
               for (int rt = 0; rt < TILE_m; rt++) {
                  // ベクトル演算を行います。
                  ctile[rt][:] += atile[rt][rc] * btile[:];
               }
            }
         }
         // 一連のベクトルのストアを行います。
         #pragma unroll
         C[r:TILE_m][c:TILE_n] = ctile[:][:];
      }
   }
      return true;
}

次の例は、#pragma simd とベクトル関数の linear および uniform 引数の使用方法を示しています。

__declspec(target(gfx))
__declspec(vector(uniform(in1), linear(i)))
// ポインター型の in1 引数は uniform として定義されています。
// in1 はベクトル関数で配列全体にアクセスできます。
// i は linear として宣言されているため、コンパイラーはより
// 効率的なコードを生成できます。
// in2v と戻り値は整数型のベクトルです。
int vfunction(int * in1, int i, int in2v)
{
    return in1[i - 1] + in2v * in1[i] + in1[i + 1];
}

int main (int argc, char* argv) 
{
    const int size = 4096;
    const int chunkSize = 32;
    const int padding = chunkSize;
    int in1[size], in2[size], out[size];

    // 初期値
    in1[:] = __sec_implicit_index(0);
    in2[:] = size - __sec_implicit_index(0);

    #pragma offload target(gfx) 
    _Cilk_for (int i = padding; i < size - padding; i+=chunkSize)
    {
        #pragma simd
        for (int j = 0; j < chunkSize; j++)
            out[i + j] = vfunction(in1, i + j, in2[i + j]);
    }

    // 以降で出力配列の使用または出力を行います。
    return 0;
}

ベクトル化の注意事項

SIMD 対応関数および #pragma simd を使用すると、スカラーコードがベクトル化されます。単純な書式で、対応するコードをベクトル化可能としてマークし、追加の最適化のヒントを与えることができます。次の要因はパフォーマンスに影響を及ぼす場合があります。

関連情報