インテル® 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 のいずれかが指定されない限り、関数の引数はすべてベクトル化されます。linear と uniform は呼び出し元と SIMD 対応関数のベクトル形式との間の特定の変換を示し、どちらも最適化に使用されます。値のベクトルの代わりに単一のスカラー値を渡すことができます。
linear は、引数の値をコンパイル時に既知のストライドで線形的にインクリメントできることを示します。デフォルトは 1 です。
uniform は、引数の値をすべての反復でブロードキャストできることを示します。
配列表記は、コンパクトなデータ並列コードの記述を可能にする強力な拡張機能です。ベクトル化が有効な場合、コンパイラーはベクトルコードを使用して配列表記を実装します。
配列表記の基本的な形式は、添字演算子に似ていますが、操作が単一の要素ではなく配列の部分に適用されるセクション演算子です。
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) を指します。
配列表記は、暗黙のインデックス変数とリダクション関数へのアクセスのような機能も提供します。
下記のコードは、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 を使用すると、スカラーコードがベクトル化されます。単純な書式で、対応するコードをベクトル化可能としてマークし、追加の最適化のヒントを与えることができます。次の要因はパフォーマンスに影響を及ぼす場合があります。
SIMD 対応関数では関数スコープの定数データはサポートされていません。複数のベクトル化された反復で共有される定数データは、uniform としてアノテーションされた関数引数で渡す必要があります。関数がインライン展開されていない限り、GRF での定数データの割り当ては禁止されます。ただし、関数がインライン展開されている場合は __declspec(vector) の情報が適用されないため、インライン展開された関数本体と #pragma simd を含むベクトル化されたループにアノテーションを付けることで、ベクトル化の動作を指示できます。
ベクトル化されたコンテキスト (ベクトル化されたループまたは SIMD 対応関数のいずれか) の内部で定義される配列および構造体形式の変数はベクトル長で複製されるため、明示的に小さなベクトル長を指定してレジスターの退避を回避しなければならない場合があります。現在、これらの配列および構造体は AoS 形式でベクトル化されます。例えば、int arr[32] は int arr[vector_length][32] になります。これらの配列および構造体へのアクセスは、ベクトルアクセスを集約/分散するように変換されます (GRF 配列の場合、しばしば逆ベクトル化されます)。または、コンパイラーがベクトル化の前にスカラーで構造体にアクセスします (各フィールドが個別の一時スカラーに変換され個別にベクトル化されます)。このため、AoS 形式のベクトル化は必ずしもカーネルのパフォーマンスに影響しません。カーネルで問題が発生する場合は、多次元配列による明示的な SoA 形式のデータレイアウトを使用し、コンパクトにベクトル・メモリー・アクセスを表現する配列表記を使用して、カーネルを書き直すことを考えてください。
特に、自動ベクトル化が適しているかどうか分からない場合は、#pragma simd や配列表記を使用してコードをベクトル化するようにコンパイラーに指示してください。
インテル® プロセッサー・グラフィックス向けのベクトル化では、64 ビットおよび 128 ビット長のショートベクトル形式は現在サポートされていません。つまり、コンパイラーは下記のコードをベクトル化しません。
ベクトル長が 4、8 または 16 要素の場合は 1 バイトのデータ型。
ベクトル長が 4 または 8 要素の場合は 2 バイトのデータ型。
ベクトル長が 4 要素の場合は 4 バイトのデータ型。
コンパイラーは、サポートされていないベクトル長が明示的に指定された場合を除き、サポートされているベクトル長を使用します。または、スカラーコードを生成して #pragma simd やベクトル関数の場合に警告を出力します。