SIMD 対応関数 (以前は要素関数と呼ばれていたもの) は、データ並列アルゴリズムを表現するための一般的な言語構造です。通常の C/C++ 関数として記述され、そのアルゴリズムはスカラー構文を使用して 1 つの要素に対する操作を指示します。1 つの要素に対する操作を行う場合は通常の C/C++ 関数として呼び出され、複数の要素に対する操作の場合はデータ並列コンテキストで呼び出されます。インテル® Cilk™ Plus では、データ並列コンテキストは配列として提供されます。
注
インテル® Cilk™ Plus は古い機能 (非推奨) です。
SIMD 対応関数を使用し、コンパイラー・オブジェクト・ファイルを以前のバージョンのコンパイラー (13.1 など) のオブジェクト・ファイルとリンクする場合、[Q]vecabilegacy コンパイラー・オプションを使用する必要があります。
SIMD 対応関数の動作
SIMD 対応関数がある場合、コンパイラーは一度の呼び出しで複数の引数に対して操作が行えるようにそのショートベクトル形式を生成します。このショート・ベクトル・バージョンは、CPU のベクトル命令セット・アーキテクチャー (ISA) を活用することで、通常の実装で個別に操作を行った場合と同じぐらい高速に複数の操作を行うことができます。SIMD 対応関数の呼び出しが、SIMD ループまたは別の SIMD 対応関数内にある場合、コンパイラーはスカラー呼び出しをその最適なショート・ベクトル・バージョンに置き換えます。さらに、cilk_for または pragma omp 構造から呼び出された場合、コンパイラーは SIMD 対応関数の異なるコピーを異なるスレッド (またはワーカー) に割り当てて同時に実行することがあります。その結果、データ並列操作は、マルチコアで利用可能な並列性とベクトル用の ISA で利用可能な並列性の両方を活用して CPU で実行されます。
さらに、cilk_for または pragma omp 構造から呼び出された場合、コンパイラーは SIMD 対応関数の異なるコピーを異なるスレッド (またはワーカー) に割り当てて同時に実行することがあります。その結果、データ並列操作は、マルチコアで利用可能な並列性とベクトル用の ISA で利用可能な並列性の両方を活用して CPU で実行されます。つまり、並列ループ (cilk_for ループまたはベクトル化された自動並列化ループ) の内側でショートベクトル関数が呼び出されると、ベクトルレベルの並列化とスレッドレベルの並列化の両方を達成できます。
SIMD 対応関数の宣言
コンパイラーがショートベクトル関数を生成するように、コード中で適切な構文を使用する必要があります。
Windows*:
次のように、__declspec(vector (clauses)) 宣言を使用します。
__declspec(vector (clauses)) return_type simd_enabled_function_name(parameters)
Linux* および macOS*:
次のように、__attribute__((vector (clauses))) 宣言を使用します。
__attribute__((vector (clauses))) return_type simd_enabled_function_name(parameters)
別の方法として、OpenMP* プラグマを使用することができます。その場合、[q または Q]openmp あるいは [q または Q]openmp-simd コンパイラー・オプションを指定する必要があります。
#pragma omp declare simd clauses
vector 宣言の clauses を利用して、パフォーマンスを向上するため、デフォルトをオーバーライドできます。SIMD 対応関数定義において、clauses はその SIMD 対応関数の 1 つ以上のショート・ベクトル・バージョンを宣言します。clauses が異なる複数の vector 宣言を 1 つの関数に含めることで、複数の異なるショート・ベクトル・バージョンを宣言することもできます。
clauses は次のように定義します。
processor(cpuid) |
指定されたプロセッサーに最適な命令、呼び出し元/呼び出し先インターフェイス、デフォルトの vectorlength を使用して、ベクトルバージョンを生成するようにコンパイラーに指示します。特に、ベクトルレジスターの幅が広いプロセッサー (core_2nd_gen_avx 以降) には、この節を使用することを強くお勧めします。
cpuid には、次のいずれかの値を指定できます。 - mic_avx512
- core_4th_gen_avx_tsx
- core_4th_gen_avx
- mic
- core_3rd_gen_avx
- core_2nd_gen_avx
- core_aes_pclmulqdq
- core_i7_sse4_2
- atom
- core_2_duo_sse4_1
- core_2_duo_ssse3
- pentium_4_sse3
- pentium_m
- pentium_4
|
vectorlength(n) / simdlen(n) (omp declare simd の場合) |
n は、32 以下の 2 の累乗であるベクトル長です。
simdlen 節は、各ルーチンの呼び出しで、スカラー関数を n 回実行するのと同じ分の計算を実行するようにコンパイラーに指示します。省略された場合、ルーチンの戻り値、引数、processor 節に応じて、コンパイラーが自動的にベクトル長を選択します。1 つのベクトル化されたコンテキストで複数のベクトルバージョンが呼び出される場合 (例えば、同じベクトルループで 2 つの異なる関数を呼び出す場合)、パフォーマンスを向上するため、明示的に同じ simdlen 値を使用したほうが良いでしょう。
|
linear(list_item[, list_item...]) ここで list_item は次のいずれかです: param[:step]、val(param[:step])、ref(param[:step])、または uval(param[:step])。 |
linear 節は、シリアル実行においてルーチンの連続した呼び出しで、param の値を step ずつ増分するようにコンパイラーに指示します。param は、指定された関数の仮引数または C++ キーワード this です。linear 節は、スカラー (非配列と非構造体)、ポインター、または C++ 参照である引数に使用できます。step はコンパイル時の整数定数式です。省略された場合、デフォルトは 1 です。
特定の引数に複数の step が指定されている場合は、コンパイル時にエラーが発生します。
複数の linear 節は結合されます。
linear 節には、次のようなバリエーションがあります。
- linear(param[:step])
- 引数が C++ 参照でない場合: ルーチンが呼び出されるループの各反復で、引数の値を step ずつ増分するようにコンパイラーに指示します。これは、下位互換性のため C++ 参照にも使用できまが、推奨しません。
- linear(val(param[:step]))
- 引数が C++ 参照の場合: ルーチンが呼び出されるループの各反復で、引数の参照値を step ずつ増分するようにコンパイラーに指示します。
- linear(uval(param[:step]))
- C++ 参照の場合: linear(val()) と同じです。linear(val()) との違いは、linear(val()) では、参照ベクトルがルーチンのベクトルバージョンに渡されますが、linear(uval()) では、1 つの参照のみが渡されます (そのため、linear(uval()) のほうがパフォーマンスの観点からは優れています)。
- linear(ref(param[:step]))
- C++ 参照の場合: 参照自体はリニアです。参照値 (計算に使用されるベクトルを形成) は、要素間の距離が step の配列のようにシーケンシャルに配置されます。
|
uniform(param [, param,]…) |
param は、指定された関数の仮引数または C++ キーワード this です。
uniform 節は、パフォーマンスを最適化するために、指定された引数の値をすべての反復にブロードキャストするようにコンパイラーに指示します。これは、通常、より最適なベクトルメモリー参照を生成するのに役立ちます。一方、uniform 節を指定しない場合、呼び出し元のループからブロードキャスト操作がホイストされることを許可します。パフォーマンスへの影響を慎重に評価してください。複数の uniform 節は結合されます。
|
mask/nomask |
mask 節と nomask 節は、ルーチンのマスク付きまたはマスクなしのベクトルバージョンを生成するようにコンパイラーに指示します。省略されると、マスク付きとマスクなしのベクトルバージョンの両方が生成されます。ルーチンが条件付きで呼び出される場合は、マスク付きバージョンが使用されます。
|
inbranch/notinbranch |
inbranch 節と notinbranch 節は、#pragma omp declare simd で使用します。inbranch 節は、上記の mask 節と同様に動作し、notinbranch 節は上記の nomask 節と同様に動作します。
|
既存の C/C++ 構文と関連するビルトイン関数 (後述の「__intel_simd_lane()」を参照) を使用して関数の中にこのコードを記述します。
ベクトル関数の使用の指定
1 つのルーチンに対して、ルーチンの異なる使用法に応じて、複数のベクトルバージョンを定義することができます。呼び出しを検出すると、コンパイラーは実引数の種類とベクトルバージョンをマッチングして、最適なバージョンを選択します。マッチングは優先度によって行われます。実引数がループ不変で、対応する仮引数に uniform 節が指定されている場合、uniform 節を含むバージョンの優先度が高くなります。linear 節の優先度は、高い順から次のとおりです: linear(uval())、linear()、linear(val())、linear(ref())。同じルーチンを呼び出す次のループの例について考えてみます。
例: OpenMP*
|
// ルーチンのプロトタイプ
#pragma omp declare simd // ユニバーサルだが 3 つのループとも 最も遅い定義と一致
#pragma omp declare simd linear(in1) linear(ref(in2)) uniform(mul) // 最初のループと一致
#pragma omp declare simd linear(ref(in2)) // 2 つ目と 3 つ目のループと一致
#pragma omp declare simd linear(ref(in2)) linear(mul) // 2 つ目のループと一致
#pragma omp declare simd linear(val(in2:2)) // 3 つ目のループと一致
extern int func(int* in1, int& in2, int mul);
int *a, *b, mul, *c;
int *ndx, nn;
...
// ループの例
for (int i = 0; i < nn; i++) {
c[i] = func(a + i, *(b + i), mul); // ループ内で 1 つ目の引数はリニアに変更される
// 2 つ目の参照もリニアに変更される
// 3 つ目の引数は不変
}
for (int i = 0; i < nn; i++) {
c[i] = func(&a[ndx[i]], b[i], i + 1); // 1 つ目の引数の値は予測不可
// 2 つ目の参照はリニアに変更される
// 3 つ目の引数はリニアに変更される
}
#pragma omp simd
for (int i = 0; i < nn; i++) {
int k = i * 2; // ベクトル化により private 変数は配列に変換される: k->k_vec[vector_length]
c[i] = func(&a[ndx[i]], k, b[i]); // 1 つ目の引数の値は予測不可
// 2 つ目の参照と値はリニアであると見なせる
// 3 つ目の引数の値は予測不可
// (一致する 2 つのバージョンの中から #pragma simd linear(val(in2:2)) が選択される)
}
|
例: インテル® Cilk™ Plus
|
// ルーチンのプロトタイプ
__declspec(vector) // ユニバーサルだが 3 つのループとも 最も遅い定義と一致
__declspec(vector(linear(in1), linear(ref(in2)), uniform(mul))) // 最初のループと一致
__declspec(vector(linear(ref(in2)))) // 2 つ目と 3 つ目のループと一致
__declspec(vector(linear(ref(in2)), linear(mul))) // 2 つ目のループと一致
__declspec(vector(linear(val(in2:2)))) // 3 つ目のループと一致
extern int func(int* in1, int& in2, int mul);
int *a, *b, mul, *c;
int *ndx, nn;
...
// ループの例
for (int i = 0; i < nn; i++) {
c[i] = func(a + i, *(b + i), mul); // ループ内で 1 つ目の引数はリニアに変更される
// 2 つ目の参照もリニアに変更される
// 3 つ目の引数は不変
}
for (int i = 0; i < nn; i++) {
c[i] = func(&a[ndx[i]], b[i], i + 1); // 1 つ目の引数の値は予測不可
// 2 つ目の参照はリニアに変更される
// 3 つ目の引数はリニアに変更される
}
#pragma simd private(k)
for (int i = 0; i < nn; i++) {
int k = i * 2; // ベクトル化により private 変数は配列に変換される: k->k_vec[vector_length]
c[i] = func(&a[ndx[i]], k, b[i]); // 1 つ目の引数の値は予測不可
// 2 つ目の参照と値はリニアであると見なせる
// 3 つ目の引数の値は予測不可
// (一致する 2 つのバージョンの中から __declspec(vector(linear(val(in2:2)))) が選択される)
}
|
SIMD 対応関数と C++
新しい C++ で SIMD 対応関数を使用する場合は注意が必要です。C++ は、コンパイル環境と実行環境について厳しい要件があり、SIMD 対応関数のような多様なセマンティクスの言語拡張とは上手く連携しないことがあります。C++ には、SIMD 対応関数の概念と関連する次の 3 つの重要な点があります: 例外処理、動的多相性、C++ 型システム。
SIMD 対応関数と例外処理
現在、SIMD コンテキストでは例外はサポートされていません: SIMD ループや SIMD 対応関数で例外をスローしたり、キャッチすることはできません。そのため、C++11 ではすべての SIMD 対応関数が noexcept と見なされます。これは、ショート・ベクトル・バージョンだけでなく、オリジナルのスカラールーチンにも影響します。関数のコンパイル時に、スロー構造と例外をスローする関数呼び出しがチェックされます。また、SIMD 対応関数呼び出しのコンパイル時にも制約を受けます。
SIMD 対応関数と動的多相性
vector 属性は、何らかの制限があるクラスの仮想関数に適用することができ、多相仮想関数呼び出しで考慮されます。vector 宣言の構文は、通常の SIMD 対応クラスメソッドと同じです: クラス宣言内のメソッド宣言に上記の vector 宣言を含めるだけです。
仮想メソッドの vector 関数属性は継承されます。オーバーライドする仮想関数の vector 属性を指定する場合、オーバーライドされる関数の vector 属性と一致していなければなりません。仮想メソッドの実装が派生クラスでオーバーライドされても、vector 宣言は継承および適用されます。親のベクトルバージョンのセットに応じて、オーバーライド用のベクトルバージョンのセットが生成されます。この規則は、親にベクトルバージョンが含まれない場合にも適用されます。仮想メソッドが SIMD 対応でない (vector 宣言を含まない) 場合、派生クラスに仮想メソッドの実装が含まれていても、派生クラスでそのメソッドを SIMD 対応にすることはできません。
仮想メソッドのベクトルバージョンのマッチングは、メソッドが呼び出されるオブジェクトの宣言 (静的な) 型によって行われます。オブジェクトの実際の (動的な) 型は、静的な型と同じか、静的な型から継承されます。
1 つのターゲット関数に制御を移す通常の関数呼び出しとは異なり、仮想関数の呼び出しターゲットはオブジェクトの動的な型に依存し、それに対してメソッドが呼び出され、クラスの仮想関数テーブルを利用して間接的に実行されます。1 つの SIMD チャンクで、複数のクラスのオブジェクト (例えば、多相コレクションの要素など) に対して仮想メソッドを呼び出すことができます。これには、1 つの SIMD チャンク内から異なるターゲットへの複数の呼び出しが必要です。以下に動作フローを示します。
- SIMD 対応仮想関数呼び出しが uniform 節を含む this 引数のバージョンと一致する場合、複数の呼び出しは必要ありません。コンパイラーは、オブジェクトの動的クラスの仮想メソッドの一致したベクトルバージョンへの 1 つの間接呼び出しを生成します。
- SIMD 対応仮想関数呼び出しが uniform 節を含まない this 引数のバージョンと一致する場合、SIMD チャンク内のすべてのオブジェクトは同じ仮想メソッドの実装を共有できる可能性があります。共有できる場合、ターゲット仮想メソッド実装の一致するベクトルバージョンが 1 回間接的に呼び出されます。
- そうでない場合、仮想呼び出しターゲットを共有するレーンはマスクされ、ループで一意の仮想呼び出しターゲットごとに、一致するベクトルバージョンに対応するマスク付きバージョンが呼び出されます。一致するベクトルバージョンのマスク付きバージョンがなく、this 引数が uniform として宣言されていない場合、その一致は拒否されます。
次に、SIMD 対応仮想関数の例を示します。
例: OpenMP*
|
struct Base {
#pragma omp declare simd
#pragma omp declare simd uniform(this)
virtual int process(int);
};
struct Child1 : Base {
// int process(int); が継承される
};
struct Child11 : Child1 {
int process(int); // 実装をオーバーライドし、vector 宣言を継承
};
struct Child2 : Base {
int process(int); // 実装をオーバーライドし、vector 宣言を継承
};
int main() {
int arr[100];
Base* c2 = new Child2();
Base* objs[100];
int res = 0;
// uniform オブジェクトに対する SIMD 対応仮想関数呼び出し
#pragma omp simd reduction(+:res)
for (int i = 0; i < 100; i++) {
res += c2->process(arr[i]); // uniform 節を含む this のバージョンと一致
// Child2::process() のベクトルバージョンを呼び出し
}
// オブジェクトの多相配列を初期化
for (int i = 0; i < 100; i++) {
if (i % 16 < 4) objs[i] = new Base();
else if (i % 16 < 8) objs[i] = new Child1();
else if (i % 12 < 12) objs[i] = new Child11();
else objs[i] = new Child2();
}
// 非 uniform オブジェクトに対する SIMD 対応仮想関数呼び出し
#pragma omp simd reduction(+:res) simdlen(8)
for (int i = 0; i < 100; i++) {
res += objs[i]->process(arr[i]); // uniform 節を含まない this と一致
// Base と Child1 は同じ 'process' 実装を共有しているため
// objs[] 配列のすべての要素について this ポインターは異なるが
// それぞれの偶数チャンク [i*16:i*16+7] の呼び出しターゲットは同じ
// 奇数チャンク [i*16+8:i*16+15] は Child11 クラスおよび Child2 クラスの
// オブジェクトで構成されているため、process() 仮想関数の対応する
// 実装の呼び出しが必要。マスク 0b00001111 (チャンクの下位レーン) で
// Child11::process() のマスク付きベクトルバージョンが呼び出され
// マスク 0b11110000 (チャンクの上位レーン) で Child2::process() の
// マスク付きベクトルバージョンが呼び出される
}
return res;
}
|
例: インテル® Cilk™ Plus
|
struct Base {
__declspec(vector)
__declspec(vector(uniform(this)))
virtual int process(int);
};
struct Child1 : Base {
// int process(int); が継承される
};
struct Child11 : Child1 {
int process(int); // 実装をオーバーライドし、vector 宣言を継承
};
struct Child2 : Base {
int process(int); // 実装をオーバーライドし、vector 宣言を継承
};
int main() {
int arr[100];
Base* c2 = new Child2();
Base* objs[100];
int res = 0;
// uniform オブジェクトに対する SIMD 対応仮想関数呼び出し
#pragma simd reduction(+:res)
for (int i = 0; i < 100; i++) {
res += c2->process(arr[i]); // uniform 節を含む this のバージョンと一致
// Child2::process() のベクトルバージョンを呼び出し
}
// オブジェクトの多相配列を初期化
for (int i = 0; i < 100; i++) {
if (i % 16 < 4) objs[i] = new Base();
else if (i % 16 < 8) objs[i] = new Child1();
else if (i % 12 < 12) objs[i] = new Child11();
else objs[i] = new Child2();
}
// 非 uniform オブジェクトに対する SIMD 対応仮想関数呼び出し
#pragma simd reduction(+:res) vectorlength(8)
for (int i = 0; i < 100; i++) {
res += objs[i]->process(arr[i]); // uniform 節を含まない this と一致
// Base と Child1 は同じ 'process' 実装を共有しているため
// objs[] 配列のすべての要素について this ポインターは異なるが
// それぞれの偶数チャンク [i*16:i*16+7] の呼び出しターゲットは同じ
// 奇数チャンク [i*16+8:i*16+15] は Child11 クラスおよび Child2 クラスの
// オブジェクトで構成されているため、process() 仮想関数の対応する
// 実装の呼び出しが必要。マスク 0b00001111 (チャンクの下位レーン) で
// Child11::process() のマスク付きベクトルバージョンが呼び出され
// マスク 0b11110000 (チャンクの上位レーン) で Child2::process() の
// マスク付きベクトルバージョンが呼び出される
}
return res;
}
|
以下は、SIMD 対応仮想関数のサポートの制限事項です。
- 仮想継承を含む多重継承は、SIMD 対応仮想メソッドを含むクラスではサポートされません。多重継承での仮想関数呼び出しは、'this' ポインターや仮想関数テーブルポインターを調整する特殊関数 thunks で行われます。現在の実装は、SIMD 対応仮想呼び出しで thunks をサポートしていません。サポートするには、thunks 自体も SIMD 対応関数になる必要がありますが、実装されていないためです。
- SIMD 対応仮想メソッドのアドレスを取得することは不可能です。SIMD 対応仮想関数のサポートには、追加の情報が必要なため、バイナリー表現が異なります。そのようなケースは、仮想メンバーへの通常のポインターを想定するコードでは適切に処理されません。
SIMD 対応関数と C++ 型システム
vector 属性は C++11 の属性で、SIMD 対応関数の関数型の一部ではありません。vector 属性は、関数自体 (関数型のインスタンス) にバインドされます。そのため、次の影響があります。
- SIMD 対応関数をテンプレート引数とするテンプレートのインスタンス化は vector 属性を認識できないため、現在、間接呼び出しを追加する std::bind のような関数ラッパー・テンプレートで vector 属性を確実に保持することは不可能です。この間接呼び出しは、コンパイラーによる最適化で直接呼び出しに置き換えられ、すべての vector 属性が保持されることがあります。
- vector 属性によってテンプレートを多重定義または特殊化する方法はありません。
- テンプレート・メタプログラミング目的で vector 属性をキャプチャーするため、関数特性を記述する方法はありません。
以下に、これらのケースの例を示します。
例: OpenMP*
|
template <int f(int)> // 関数の値テンプレート - 関数型ではなく関数をキャプチャー
int caller1(int x[100]) {
int res = 0;
#pragma omp simd reduction(+:res)
for (int i = 0; i < 100; i++) {
res += f(x[i]); // インスタンス化のときに関数をここに配置
}
return res;
}
template <typename F> // 汎用関数型テンプレート - ファンクターまたは
// 関数型全体のオブジェクト・タイプをキャプチャー
// 関数型に vector 属性が含まれる場合にキャプチャーおよび
// 適用可能だが、現在は利用できない
int caller2(F f, int x[100]) {
int res = 0;
#pragma omp simd reduction(+:res)
for (int i = 0; i < 100; i++) {
res += f(x[i]); // 一致する関数 ‘f’ を間接的に呼び出す
// 一致する f.operator() を直接呼び出す
}
return res;
}
template <typename RET, typename ARG> // 型分解テンプレートは
// 引数と戻り型をキャプチャー
// vector 属性は関数型の一部であっても失われる
int caller3(RET (*f)(ARG), int x[100]) {
int res = 0;
#pragma omp simd reduction(+:res)
for (int i = 0; i < 100; i++) {
res += f(x[i]); // 一致する関数 'f' を間接的に呼び出す
}
return res;
}
#pragma omp declare simd
int function(int x); // SIMD 対応関数
int nv_function(int x); // 通常のスカラー関数
struct functor { // SIMD 対応 operator() を含むファンクター・クラス
#pragma omp declare simd
int operator()(int x);
};
int arr[100];
int main() {
int res;
#pragma noinline
res = caller1<function>(arr); // function() 向けにインスタンス化し
// ショート・ベクトル・バージョンを呼び出す
#pragma noinline
res += caller1<nv_function>(arr); // nv_function() 向けにインスタンス化する
#pragma noinline
res += caller2(function, arr); // int(*)(int) 型向けにインスタンス化し
// スカラーバージョンの function() を間接的に呼び出す
#pragma noinline
res += caller2(nv_function, arr); // 上記の nv_function と同じインスタンスを呼び出す
#pragma noinline
res += caller2(functor(), arr); // ファンクター型向けにインスタンス化し
// functor::operator() のショート・ベクトル・バージョンを呼び出す
#pragma noinline
res += caller3(function, arr); // <int, int> 型向けにインスタンス化し
// スカラーバージョンの function() を間接的に呼び出す
#pragma noinline
res += caller3(nv_function, arr); // 上記の nv_function と同じインスタンスを呼び出す
return res;
}
|
例: インテル® Cilk™ Plus
|
template <int f(int)> // 関数の値テンプレート - 関数型ではなく関数をキャプチャー
int caller1(int x[100]) {
int res = 0;
#pragma simd reduction(+:res)
for (int i = 0; i < 100; i++) {
res += f(x[i]); // インスタンス化のときに関数をここに配置
}
return res;
}
template <typename F> // 汎用関数型テンプレート - ファンクターまたは
// 関数型全体のオブジェクト・タイプをキャプチャー
// 関数型に vector 属性が含まれる場合にキャプチャーおよび
// 適用可能だが、現在は利用できない
int caller2(F f, int x[100]) {
int res = 0;
#pragma simd reduction(+:res)
for (int i = 0; i < 100; i++) {
res += f(x[i]); // 一致する関数 ‘f’ を間接的に呼び出す
// 一致する f.operator() を直接呼び出す
}
return res;
}
template <typename RET, typename ARG> // 型分解テンプレートは
// 引数と戻り型をキャプチャー
// vector 属性は関数型の一部であっても失われる
int caller3(RET (*f)(ARG), int x[100]) {
int res = 0;
#pragma simd reduction(+:res)
for (int i = 0; i < 100; i++) {
res += f(x[i]); // 一致する関数 ‘f’ を間接的に呼び出す
}
return res;
}
__declspec(vector) int function(int x); // SIMD 対応関数
int nv_function(int x); // 通常のスカラー関数
struct functor { // SIMD 対応 operator() を含むファンクター・クラス
__declspec(vector)
int operator()(int x);
};
int arr[100];
int main() {
int res;
#pragma noinline
res = caller1<function>(arr); // function() 向けにインスタンス化し
// ショート・ベクトル・バージョンを呼び出す
#pragma noinline
res += caller1<nv_function>(arr); // nv_function() 向けにインスタンス化する
#pragma noinline
res += caller2(function, arr); // int(*)(int) 型向けにインスタンス化し
// スカラーバージョンの function() を間接的に呼び出す
#pragma noinline
res += caller2(nv_function, arr); // 上記の nv_function と同じ
// インスタンスを呼び出す
#pragma noinline
res += caller2(functor(), arr); // ファンクター型向けにインスタンス化し
// functor::operator() のショート・ベクトル・バージョンを呼び出す
#pragma noinline
res += caller3(function, arr); // <int, int> 型向けにインスタンス化し
// スカラーバージョンの function() を間接的に呼び出す
#pragma noinline
res += caller3(nv_function, arr); // 上記の nv_function と同じインスタンスを呼び出す
return res;
}
|
注
注: caller1、caller2、および caller3 の呼び出しがインライン展開されると、コンパイラーはすべてのケースで間接呼び出しを直接呼び出しに置き換えることができます。このケースでは、SIMD ループでショート・ベクトル・バージョンのマッチングを行い、function() の直接呼び出しを置き換えることで、caller2(function, arr) と caller3(function, arr) はどちらも関数のショート・ベクトル・バージョンを呼び出します。
並列コンテキストでの SIMD 対応関数の呼び出し
通常、SIMD 対応関数で仮引数としてスカラー引数が指定されている場合、その呼び出しは配列を提供します。この配列は、インテル® Cilk™ Plus の配列表記を使用して簡潔に提供することができます。また、_Cilk_for ループから SIMD 対応関数を呼び出すこともできます。
次の 2 つの構文は、コンパイラーに特殊なベクトル命令を発行させることで命令レベルの並列処理を実現します。
a[:] = ef_add(b[:],c[:]); // 配列 a、b、c 全体に対して処理を行う
a[0:n:s] = ef_add(b[0:n:s],c[0:n:s]); // 配列表記構造で n (長さ) と s (ストライド) を指定する
データ並列コンテキストで SIMD 対応関数を呼び出し、複数のコアとプロセッサーを使用するには _Cilk_for を使用します。
_Cilk_for (j = 0; j < n; ++j) {
a[j] = ef_add(b[j],c[j]);
}
注
_Cilk_for 呼び出し構文を使用するコードのみが利用可能なすべての並列性を使用できます。
通常の for ループからの SIMD 対応関数の呼び出しおよび配列表記構文を使用すると、各反復でショートベクトル関数が呼び出され、この呼び出しはマルチコアを活用することなくシリアルループで行われます。利用可能なすべての並列性を使用するには、インテル® Cilk™ Plus キーワード (cilk_for、cilk_spawn など) または OpenMP* の使用を考慮すべきです。
__intel_simd_lane() ビルトイン関数の使用
ベクトル化されたループから呼び出される場合、__intel_simd_lane() ビルトイン関数は、SIMD ベクトル内の現在の "レーン ID" を示す 0 ~ vectorlength - 1 の範囲の値を返します。ループがベクトル化されていない場合、__intel_simd_lane() は 0 を返します。__intel_simd_lane() は、明示的なベクトル・プログラミング構造の範囲外で呼び出さないでください。自動ベクトル化を妨げる可能性があります。また、そのような呼び出しは通常、0 ~ vectorlength-1 の範囲内の値ではなく 0 を返します。
__intel_simd_lane() の使用法を示す次の例について考えてみます。
void accumulate(float *a, float *b, float *c, d){
*a+=sin(d);
*b+=cos(d);
*c+=log(d);
}
for (i=low; i<high; i++){
accumulate(&suma, &sumb, &sumc, d[i]);
}
まず、__intel_simd_lane() を使用せずにインテル® Cilk™ Plus のベクトルコードに変換します。
例: OpenMP*
|
#define VL 16
#pragma omp declare simd uniform(a,b,c) linear(i)
void accumulate(float *a, float *b, float *c, d, i){
a[i & (VL-1)]+=sin(d);
b[i & (VL-1)]+=cos(d);
c[i & (VL-1)]+=log(d);
}
float a[VL] = {0.0f};
float b[VL] = {0.0f};
float c[VL] = {0.0f};
#pragma omp simd for simdlen(VL)
for (i=low; i<high; i++){
accumulate(a, b, c, d[i], i);
}
for(i=0;i<VL;i++){
suma += a[i];
sumb += b[i];
sumc += c[i];
}
|
例: インテル® Cilk™ Plus
|
#define VL 16
__declspec(vector(uniform(a,b,c), linear(i)))
void accumulate(float *a, float *b, float *c, d, i){
a[i & (VL-1)]+=sin(d);
b[i & (VL-1)]+=cos(d);
c[i & (VL-1)]+=log(d);
}
float a[VL] = {0.0f};
float b[VL] = {0.0f};
float c[VL] = {0.0f};
#pragma omp simd for safelen(VL)
for (i=low; i<high; i++){
accumulate(a, b, c, d[i], i);
}
for(i=0;i<VL;i++){
suma += a[i];
sumb += b[i];
sumc += c[i];
}
|
SIMD 対応関数 accumulate() の配列 A、B、C への参照で生じるギャザー/スキャッター形式のメモリーアドレス指定は、パフォーマンスを大幅に低下させ、変換した意味がなくなってしまいます。このペナルティーを回避するため、次のように __intel_simd_lane() ビルトイン関数を使用します。
例: OpenMP*
|
#pragma omp declare simd uniform(a,b,c) aligned(a,b,c)
void accumulate(float *a, float *b, float *c, float d){
// "ループ・インデックス"、VL は不要。
a[__intel_simd_lane()]+=sin(d);
b[__intel_simd_lane()]+=cos(d);
c[__intel_simd_lane()]+=log(d);
}
#define VL 16 // 実際の SIMD コードではベクトル長 4 を使用している可能性がある
float a[VL] = {0.0f};
float b[VL] = {0.0f};
float c[VL] = {0.0f};
#pragma omp simd for simdlen(VL)
for (i=low; i<high; i++){
// コンパイル時に low が 0 と判明している場合、"i & (VL-1)" は
// 呼び出し元で __intel_simd_lane() の意図したとおりになる
accumulate(a, b, c, d[i]);
}
for(i=0;i<VL;i++){
suma += a[i];
sumb += b[i];
sumc += c[i];
}
|
例: インテル® Cilk™ Plus
|
__declspec(vector(uniform(a,b,c),aligned(a,b,c)))
void accumulate(float *a, float *b, float *c, d){
// "ループ・インデックス"、VL は不要。
a[__intel_simd_lane()]+=sin(d);
b[__intel_simd_lane()]+=cos(d);
c[__intel_simd_lane()]+=log(d);
}
#define VL 16 // 実際の SIMD コードではベクトル長 4 を使用している可能性がある
float a[VL] = {0.0f};
float b[VL] = {0.0f};
float c[VL] = {0.0f};
#pragma omp simd for safe_veclen(VL)
for (i=low; i<high; i++){
// コンパイル時に low が 0 と判明している場合、"i & (VL-1)" は
// 呼び出し元で __intel_simd_lane() の意図したとおりになる
accumulate(a, b, c, d[i]);
}
for(i=0;i<VL;i++){
suma += a[i];
sumb += b[i];
sumc += c[i];
}
|
__intel_simd_lane() を使用することで、accumulate() 内の配列参照をユニットストライドにできます。
制限事項
次の言語構造は SIMD 対応関数内で許可されていません。
- GOTO 文
- 16 以上の case 文を含む switch 文
- class や struct に対する操作 (メンバーの選択を除く)
- _Cilk_spawn キーワードとインテル® Cilk™ Plus のぺディグリーの更新
- すべての OpenMP* 構文