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

SIMD 対応関数ポインター

SIMD 対応関数 (以前は要素関数と呼ばれていたもの) は、データ並列アルゴリズムを表現するための一般的な言語構造です。通常の C/C++ 関数として記述され、そのアルゴリズムはスカラー構文を使用して 1 つの要素に対する操作を指示します。1 つの要素に対する操作を行う場合は通常の C/C++ 関数として呼び出され、複数の要素に対する操作の場合はデータ並列コンテキストで呼び出されます。インテル® Cilk™ Plus では、データ並列コンテキストは SIMD ループです。

インテル® Cilk™ Plus は古い機能 (非推奨) です。

場合によっては、SIMD 対応関数のポインターがあると便利ですが、特別な方法を用いなければ、関数ポインターはスカラー関数をポイントし、スカラー関数のショート・ベクトル・バージョンを呼び出す手段がないため、SIMD 対応関数のベクトル処理を利用できません。

SIMD 対応関数のベクトルバージョンを間接的に呼び出せるように、SIMD 対応関数ポインターが追加されました。SIMD 対応関数ポインターは、通常の関数ポインターとは互換性がない特殊なポインターです。スカラー関数に加えて、ショート・ベクトル・バージョンのセットをすべて参照できます。関数ポインターとの互換性の問題は、特に C++ コードにおいて不適切な使用を引き起こすリスクを伴います。そのため、デフォルトではベクトル関数ポインターのサポートは無効になっています。

SIMD 対応関数ポインターのサポートを有効にするには、次のコンパイラー・オプションを使用します: /Qsimd-function-pointers (Windows*) または -simd-function-pointers (Linux* および macOS*)。

SIMD 対応関数ポインターは、SIMD ループや別の SIMD 対応関数から関数の適切なベクトルバージョンを間接的に呼び出せるように、SIMD 対応関数のアドレスを保持します。

/Qsimd-function-pointers- (Windows*) または -no-simd-function-pointers (Linux* および macOS*) により無効に設定されている場合、vector 属性 (__declspec(vector)__attribute__((vector))、または #pragma omp declare simd) は、関数宣言と関数定義でのみ指定できます。その他の場所で指定すると警告メッセージが出力され、無視されます。

SIMD 対応関数ポインターの仕組み

SIMD 対応関数がある場合、コンパイラーは一度の呼び出しで複数の引数に対して操作が行えるようにそのショートベクトル形式を生成します。ショート・ベクトル・バージョンは、CPU のベクトル命令セットアーキテクチャー (ISA) を利用して、スカラーバージョンが 1 つの操作を行う時間で、複数の同じ操作を実行することができます。SIMD ループや別の SIMD 対応関数で SIMD 対応関数が呼び出されると、コンパイラーはスカラー呼び出しを利用可能なベクトルバージョンの中から最適化ものに置き換えます。

SIMD 対応関数の間接呼び出しは同様に処理されますが、間接呼び出しの時点では実際の呼び出しターゲットが不明なため、ターゲット関数ではなく、利用可能なバージョンのセットが関数ポインター変数に関連付けられていないければなりません。つまり、SIMD 対応関数ポインターによって参照されるすべての SIMD 対応関数には、ポインターで宣言されているバージョンのセットがなければなりません。

SIMD 対応関数ポインター変数の宣言

コンパイラーが SIMD 対応関数へのポインターを生成できるように、コードでヒントを提供する必要があります。

Windows*:

次のように、__declspec(vector (clauses)) 属性を使用します。

__declspec(vector (clauses)) return_type (*function_pointer_name) (parameters)

Linux* および macOS*:

次のように、__attribute__((vector (clauses))) 属性を使用します。

__attribute__((vector (clauses))) return_type (*function_pointer_name) (parameters)

別の方法として、OpenMP* 4.5 の #pragma omp declare simd を使用することができます。その場合、[q または Q]openmp あるいは [q または Q]openmp-simd コンパイラー・オプションを指定する必要があります。

節については、「SIMD 対応関数」に説明があります。

ポインターでの vector 関数属性の使用

1 つの SIMD 対応関数ポインターに、ポインターにより呼び出されるターゲット関数で利用可能なすべてのバージョンに対応した、複数の vector 属性を関連付けることができます。通常、属性はループにおける関数ポインターの使用法を示します。間接呼び出しを検出すると、コンパイラーは実引数の種類と関数ポインターで宣言されているベクトルバージョンをマッチングして、最適なバージョンを選択します。マッチングは、直接呼び出しと同じ方法で行われます (詳細は、「SIMD 対応関数」を参照してください)。ベクトル関数ポインター宣言と間接呼び出しを含むループの例について考えてみます。

例: 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 つ目のループと一致
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 つ目のループと一致
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)))) が選択される)
   }

呼び出しで使用する前に、関数ポインターに関数のアドレスまたは別の関数ポインターを代入する必要があります。通常の関数ポインターと同様に、ベクトル関数ポインターも代入および初期化において互換性を保持すべきです。次のセクションでは、互換性に関する規則を説明します。

ベクトル関数ポインターの互換性

ポインター代入の互換性は次のように定義されます。

  1. SIMD 対応関数ポインターに関数のアドレスを代入する場合、その関数は通常の C/C++ 規則に従って、ポインターと互換性がなければなりません。また、SIMD 対応で、関数で宣言されているベクトルバージョンのセットは、ポインターで宣言されているベクトルバージョンのスーパーセットでなければなりません。これには、初期化処理と SIMD 対応関数アドレスの引数渡しが含まれます。
  2. SIMD 対応関数ポインターに別の関数ポインターを代入する場合、ソースポインターは通常の C/C++ 規則に従って、デスティネーション関数ポインターと互換性がなければなりません。また、SIMD 対応で、ソースポインターで宣言されているベクトルバージョンのセットは、デスティネーション・ポインターで宣言されているベクトルバージョンのセットと同じでなければなりません。これには、初期化処理と SIMD 対応関数ポインターの引数渡しが含まれます。
  3. 通常の (非 SIMD 対応) 関数ポインターに SIMD 対応関数のアドレスを代入する場合、スカラー関数のアドレスが代入されます。このポインターからベクトルバージョンを呼び出すことはできません。また、規則 2 の SIMD 対応関数ポインターとして解釈したり、変換することはできません。
  4. 通常の (非 SIMD 対応) 関数ポインターに C/C++ 規則に従って SIMD 対応関数ポインターを代入する場合、スカラー関数のアドレスを抽出し代入することで、代入の右辺 (RHS) が暗黙で動的にキャストされます。このポインターからベクトルバージョンを呼び出すことはできません。また、規則 2 の SIMD 対応関数ポインターとして解釈したり、変換することはできません。

SIMD 対応関数ポインターと通常の関数ポインターは、バイナリー互換性がありません。これらを混在させると、予期しない深刻な結果を引き起こす可能性があります。コンパイラーは、C/C++ 言語標準に従って互換性をチェックしますが、関数ポインターを未宣言の関数へ渡したり、関数ポインターを変数の引数として渡すようなケースはチェックできません。このようなコンテキストでは、SIMD 対応関数ポインターの使用を控えたほうが良いでしょう。C++ 型システムにおける追加の考慮事項については、前述の「SIMD 対応関数ポインター」と後述の「C++ 型システム」に説明があります。

規則 4 のキャスト操作を利用して、SIMD 対応関数ポインターにスカラー関数ポインターを代入することは可能ですが、SIMD 対応関数ポインターはスカラー関数ポインターを参照できません。

宣言と代入の例: OpenMP*

// ポインター宣言
#pragma omp declare simd
int (*ptr1)(int*, int);
#pragma omp declare simd
int (*ptr1a)(int*, int);

#pragma omp declare simd
#pragma omp declare simd linear(a)
typedef int (*fptr_t2)(int* a, int b);

typedef int (*fptr_t3)(int*, int);

fptr_t2 ptr2, ptr2a;
fptr_t3 ptr3;

// 関数宣言
#pragma omp declare simd
int func1(int* x, int b);

#pragma omp declare simd
#pragma omp declare simd linear(x)
int func2(int* x, int b);

#pragma omp declare simd
#pragma omp declare simd linear(x)
int func3(float* x, int b);

//--------------------------------------
  // 許可された代入
  ptr1 = func1;  // 同じプロトタイプとベクトルバージョン
  ptr2 = func2;  // 同じプロトタイプとベクトルバージョン
  ptr1a = ptr1;  // 同じプロトタイプとベクトルバージョン
  ptr1a = func2; // 同じプロトタイプで関数のベクトルバージョンにポインターのベクトルバージョンがすべて含まれる

  ptr3 = func1; // 同じプロトタイプのスカラーポインター - スカラー func1 を使用
  ptr3 = func2; // 同じプロトタイプのスカラーポインター - スカラー func2 を使用
  ptr3 = ptr1;  // 同じプロトタイプのスカラーポインター - ベクトルポインターからスカラーポインターへの暗黙の変換
  ptr3 = ptr2;  // 同じプロトタイプのスカラーポインター - ベクトルポインターからスカラーポインターへの暗黙の変換

  // 許可されていない代入
  ptr2 = func1; // 関数のベクトルバージョンにポインターのベクトルバージョンがすべて含まれない
  ptr2 = func3; // ベクトルバージョンは一致するがプロトタイプが不一致
  ptr1 = func3; // ベクトルバージョンは一致するがプロトタイプが不一致
  ptr3 = func3; // プロトタイプの不一致
  ptr1 = ptr2;  // ポインターのベクトルバージョンは同じでなければならない
  ptr2 = ptr3;  // ポインターのベクトルバージョンは同じでなければならない

宣言と代入の例: インテル® Cilk™ Plus

// ポインター宣言
__declspec(vector)
int (*ptr1)(int*, int);
__declspec(vector)
int (*ptr1a)(int*, int);

typedef int
__declspec(vector)
__declspec(vector(linear(a))
(*fptr_t2)(int* a, int b);

typedef int
(*fptr_t3)(int*, int);

fptr_t2 ptr2, ptr2a;
fptr_t3 ptr3;

// 関数宣言
__declspec(vector)
int func1(int* x, int b);

__declspec(vector)
__declspec(vector(linear(x))
int func2(int* x, int b);

__declspec(vector)
__declspec(vector(linear(x))
int func3(float* x, int b);

//--------------------------------------
  // 許可された代入
  ptr1 = func1;  // 同じプロトタイプとベクトルバージョン
  ptr2 = func2;  // 同じプロトタイプとベクトルバージョン
  ptr1a = ptr1;  // 同じプロトタイプとベクトルバージョン
  ptr1a = func2; // 同じプロトタイプで関数のベクトルバージョンにポインターのベクトルバージョンがすべて含まれる

  ptr3 = func1; // 同じプロトタイプのスカラーポインター - スカラー func1 を使用
  ptr3 = func2; // 同じプロトタイプのスカラーポインター - スカラー func2 を使用
  ptr3 = ptr1;  // 同じプロトタイプのスカラーポインター - ベクトルポインターからスカラーポインターへの暗黙の変換
  ptr3 = ptr2;  // 同じプロトタイプのスカラーポインター - ベクトルポインターからスカラーポインターへの暗黙の変換

  // 許可されていない代入 
  ptr2 = func1; // 関数のベクトルバージョンにポインターのベクトルバージョンがすべて含まれない
  ptr2 = func3; // ベクトルバージョンは一致するがプロトタイプが不一致
  ptr1 = func3; // ベクトルバージョンは一致するがプロトタイプが不一致
  ptr3 = func3; // プロトタイプの不一致
  ptr1 = ptr2;  // ポインターのベクトルバージョンは同じでなければならない
  ptr2 = ptr3;  // ポインターのベクトルバージョンは同じでなければならない

呼び出しシーケンス

1 つのターゲット関数に制御を移す通常の関数呼び出しとは異なり、間接呼び出しのターゲットは関数ポインターの動的コンテンツに依存します。ループでは、ベクトル化されたループの反復や呼び出しを実行する SIMD 対応関数のレーンにより、呼び出しターゲットが異なる可能性があります。ベクトル化されると、関数呼び出しは、1 つの SIMD チャンク内で異なるターゲットを呼び出す可能性があります。以下に動作フローを示します。

  1. ベクトル関数ポインターがインテル® Cilk™ Plus や OpenMP* 仕様により均一である場合、あるいはコンパイラーが均一であると判断できる場合、複数の呼び出しは不要です。コンパイラーは、ポインターが利用可能な最適なベクトルバージョンへの 1 つの間接呼び出しを生成します。
  2. コンパイル時にベクトル関数ポインターが均一かどうか不明な場合であっても、SIMD チャンクのポインター値がすべて同じである可能性があります。これは実行時にチェックされ、同じ場合は、一致するベクトルバージョンが間接的に 1 回呼び出されます。
  3. そうでない場合、同じ関数ポインター値を共有するレーン (ターゲット) はマスクされ、ループで一意の呼び出しターゲットごとに、一致するベクトルバージョンに対応するマスク付きバージョンが呼び出されます。一致するベクトルバージョンのマスク付きバージョンがなく、コンパイラーにより関数ポインターが均一でないと証明された場合、その一致は拒否され、コンパイラーは呼び出しをシリアル化 (つまり、複数のスカラー呼び出しを生成) します。

例: OpenMP*

// ポインターの型宣言
#pragma omp declare simd
typedef int (*fptr_t1)(int*, int);

// 関数宣言
#pragma omp declare simd
int func1(int* x, int b);

// ベクトル関数ポインターの使用
fptr_t1 *fptr_array;   // ベクトル関数ポインターの配列
void foo(int N, int *x, int y){
  fptr_t1 ptr1 = func1;
#pragma omp simd
  for (int i = 0; i < N; i++) {
    ptr1(x+i, y);  // OpenMP* 規則により ptr1 は均一
    fptr_t1 ptr1a = ptr1;
    ptr1a(x+i, y); // コンパイラーは ptr1a が均一であると証明できる
    fptr_t1 ptr1b = fptr_array[i];
    ptr1b(x+i,y);  // ptr1b は均一かどうか不明
  }
}

例: インテル® Cilk™ Plus

// ポインターの型宣言
typedef int
__declspec(vector)
(*fptr_t1)(int*, int);

// 関数宣言
__declspec(vector)
int func1(int* x, int b);

// ベクトル関数ポインターの使用
fptr_t1 *fptr_array;   // ベクトル関数ポインターの配列
void foo(int N, int *x, int y){
  fptr_t1 ptr1 = func1;
#pragma omp simd
  for (int i = 0; i < N; i++) {
    ptr1(x+i, y);  // OpenMP* 規則により ptr1 は均一
    fptr_t1 ptr1a = ptr1;
    ptr1a(x+i, y); // コンパイラーは ptr1a が均一であると証明できる
    fptr_t1 ptr1b = fptr_array[i];
    ptr1b(x+i,y);  // ptr1b は均一かどうか不明
  }
}

SIMD 対応関数ポインターと C++ 型システム

新しい C++ で SIMD 対応関数ポインターを使用する場合は注意が必要です。C++ は、コンパイル環境と実行環境について厳しい要件があり、SIMD 対応関数ポインターのような多様なセマンティクスの言語拡張とは上手く連携しないことがあります。SIMD 対応関数ポインターの vector 属性は C++11 の属性で、ポインター型の一部ではありません。同じ型であっても vector 属性が含まれていない別のポインターとは、互換性がありません。vector 属性は、ポインター型ではなく、引数や関数引数 (ポインター型のインスタンス) にバインドされます。SIMD 対応関数ポインター修飾子の有無にかかわらず、その関数ポインターのポインターの型は同じです。そのため、次の影響があります。

例: OpenMP*

// ポインターの型宣言と宣言
typedef int
(*fptr_t)(int*, int);

#pragma omp declare simd
typedef int (*fptr_t1)(int*, int);

#pragma omp declare simd
#pragma omp declare simd linear(x)
typedef int (*fptr_t2)(int* a, int b);

fptr_t ptr
fptr_t1 ptr1
fptr_t2 ptr2

// SIMD 対応関数修飾子のみ異なる関数プロトタイプ
// これらはすべて同じマングル名になる
void foo(fptr_t);
void foo(fptr_t1);
void foo(fptr_t2);

// テンプレートのインスタンス化
template <typename T>
void bar(T);
…
  bar(fptr);          // bar<fptr_t>
  bar(fptr1);         // bar<fptr_t>
  bar(fptr2);         // bar<fptr_t>

例: インテル® Cilk™ Plus

// ポインターの型宣言と宣言
typedef int
(*fptr_t)(int*, int);

typedef int
__declspec(vector)
(*fptr_t1)(int*, int);

typedef int
__declspec(vector)
__declspec(vector(linear(a)))
 (*fptr_t2)(int* a, int b);

fptr_t ptr
fptr_t1 ptr1
fptr_t2 ptr2

// SIMD 対応関数修飾子のみ異なる関数プロトタイプ
// これらはすべて同じマングル名になる
void foo(fptr_t);
void foo(fptr_t1);
void foo(fptr_t2);

// テンプレートのインスタンス化
template <typename T>
void bar(T);
…
  bar(fptr);          // bar<fptr_t>
  bar(fptr1);         // bar<fptr_t>
  bar(fptr2);         // bar<fptr_t>

並列コンテキストでの SIMD 対応関数の間接呼び出し

通常、SIMD 対応関数の直接または間接呼び出しでは、仮引数としてスカラー引数が指定されている場合、配列が提供されます。この配列は、インテル® Cilk™ Plus の配列表記を使用して簡潔に提供することができます。また、_Cilk_for ループから SIMD 対応関数を呼び出すこともできます。

次の 2 つの構文は、コンパイラーに特殊なベクトル命令を発行させることで命令レベルの並列処理を実現します。

例: OpenMP*

#pragma omp declare simd
float (**vf_ptr)(float, float);

// 配列 a、b、c 全体に対して処理を行う
a[:] = vf_ptr[:] (b[:],c[:]);    

// 配列表記構造で n (長さ) と s (ストライド) を指定する
a[0:n:s] = vf_ptr[0:n:s] (b[0:n:s],c[0:n:s]); 

例: インテル® Cilk™ Plus

__declspec(vector)
float (**vf_ptr)(float, float);

// 配列 a、b、c 全体に対して処理を行う
a[:] = vf_ptr[:] (b[:],c[:]);    

// 配列表記構造で n (長さ) と s (ストライド) を指定する
a[0:n:s] = vf_ptr[0:n:s] (b[0:n:s],c[0:n:s]); 

データ並列コンテキストで SIMD 対応関数を呼び出し、複数のコアとプロセッサーを使用するには _Cilk_for を使用します。

_Cilk_for (j = 0; j < n; ++j) {
  a[j] = vf_ptr[j](b[j],c[j]);
}

_Cilk_for を使用する呼び出し構文のみ、利用可能なすべての並列性を引き出すことができます。通常の for ループからの SIMD 対応関数の呼び出しおよび配列表記構文を使用すると、各反復でショート・ベクトル・バージョンが呼び出され、この呼び出しはマルチコアを活用することなくシリアルループで行われます。

関連情報