この記事は、インテルの The Parallel Universe Magazine 25 号に収録されている、最新のインテル® C++ コンパイラーでサポートされたベクトル化方式の SIMD 対応関数の間接呼び出しに関する章を抜粋翻訳したものです。
Parallel Universe 22 号では、OpenMP* 4.0 仕様の SIMD サポート機能について取り上げました。OpenMP* 4.0 の主要な機能の 1 つは、ユーザー定義関数 (SIMD 対応関数) のベクトル化が可能なことと、ベクトル化されたコードからベクトル化された SIMD 対応関数の呼び出しが可能なことです。SIMD 対応関数は、通常の関数およびサブルーチンでベクトル命令を使用して多くの要素のデータ並列コンテキスト (ベクトルループの内部または別の SIMD 対応関数の内部) で同時に操作を行うことにより、SIMD ハードウェアのプログラミング環境を大幅に向上します。
これまでプログラマーにより記述されていた、実行ファイルでスカラー関数呼び出しをベクトル化された関数に置き換えるメカニズムは、どのスカラー関数が呼び出されたか判断するコンパイル時の静的な情報に依存していました。その結果、多くの場合、ベクトル化された関数は直接呼び出しのみに制限されていました。
関数ポインターや仮想関数呼び出しによる間接関数呼び出しは、C++ のような現代のプログラミング言語の大きな特徴です。関数ポインターの主な利点は、実行時のポインターに基づいて実行する関数を選択する簡単な方法を提供して、アプリケーション・コードを単純化できることです。間接関数呼び出しの性質により、実際に呼び出される関数は実行時に決定されます。そのため、インテル® C++ コンパイラー 16.0 までは、これらの呼び出しがベクトル化の前に直接呼び出しに最適化されない限り、ベクトル化されたコンテキスト内の間接呼び出しはすべてシリアル化されていました。コンパイラーはこれらの間接呼び出しをベクトル化できず、SIMD 対応ハードウェアのベクトル機能を活用できないため、間接的に呼び出された SIMD 対応関数を実行すると大幅なボトルネックが発生していました。
インテル® C++ コンパイラー 17.0 (インテル® Parallel Studio XE 2017 に含まれる) では、ベクトル化方式の SIMD 対応関数の間接呼び出しがサポートされました。また、SIMD 対応関数のポインターを宣言する新しい構文も追加されました。互換性規則のセットは、コンパイル時にスカラー関数の同一性を実際に解決するのではなく、異なるマッピングでスカラー関数をベクトル化します。SIMD 対応関数の間接呼び出しを宣言して使用する例を次に示します。
1 つの SIMD 対応関数ポインターに、ポインターにより呼び出されるターゲット関数で利用可能なすべてのバージョンに対応した、複数の vector 属性を関連付けることができます。間接呼び出しを検出すると、コンパイラーは実引数の種類と関数ポインターで宣言されているベクトルバージョンを比較して、最適なバージョンを選択します。ベクトル関数ポインター宣言と間接呼び出しを含む図 1 の例について考えてみます。
// ポインター宣言 // ユニバーサルだが 3 つのループとも最も遅い定義と一致 __declspec(vector) // 最初のループと一致 __declspec(vector(linear(in1), linear(ref(in2)), uniform(mul))) // 2 つ目と 3 つ目のループと一致 __declspec(vector(linear(ref(in2)))) // 2 つ目のループと一致 __declspec(vector(linear(ref(in2)), linear(mul))) // 3 つ目のループと一致 __declspec(vector(linear(val(in2:2)))) int (*func)(int* in1, int& in2, int mul); int *a, *b, mul, *c; int *ndx, nn; ... // ループの例 for (int i = 0; i < nn; i++) { /* * ループ内で 1 つ目の引数はリニアに変更される * 2 つ目の参照もリニアに変更される * 3 つ目の引数は不変 */ c[i] = func(a + i, *(b + i), mul); } for (int i = 0; i < nn; i++) { /* * 1 つ目の引数の値は予測不可 * 2 つ目の参照はリニアに変更される * 3 つ目の引数はリニアに変更される */ c[i] = func(&a[ndx[i]], b[i], i + 1); } #pragma simd private(k) for (int i = 0; i < nn; i++) { /* * ベクトル化により private 変数は配列に変換される: * k->k_vec[vector_length] */ int k = i * 2; /* * 1 つ目の引数の値は予測不可 * 2 つ目の参照と値はリニアであると見なせる * 3 つ目の引数の値は予測不可 * (__declspec(vector(linear(val(in2:2)))) が * 一致する 2 つのバージョンの中から選択される) */ c[i] = func(&a[ndx[i]], k, b[i]); }
リスト 1. ベクトル関数ポインター宣言と間接呼び出しを含むループ