「さよなら Cilk Plus」移行の手順 – 明日のためのその 3 (最終回)

同カテゴリーの次の記事

インテル® AVX-512 でベクトル化のパフォーマンスを向上する

2017 年 9 月に発表されたインテル® C++ コンパイラー バージョン 18.0 で、インテル® Cilk™ Plus のサポートが将来のインテル® C++ コンパイラーのバージョンでは打ち切られることが正式になりました (V18 ではインテル® Cilk™ Plus の構文を含むソースをコンパイルすると、コンパイラーから警告が出ます))。
 

そして、OpenMP* やインテル® スレッディング・ビルディング・ブロック (インテル® TBB) など他のテクノロジーへの移行が推奨されるようになりました。gcc のブランチにあるインテル® Cilk™ Plus のサポートがどうなるかは不明ですが、日本で唯一のインテル® Cilk™ Plus 書籍 (たぶん) の著者としては若干複雑な思いです。

本稿では、4 回に分けてインテル® Cilk™ Plus のすべての機能が既存の他のテクノロジーへ移行が可能であるか検討しています。第 2 回目では、インテル® Cilk™ Plus の 3 つの キーワードとレデューサーを使用しているケースを検討し、第 3 回目は、並列表記 (Array Section) とヘルパー関数を使用するケースを検討しました。最終回では、#pragma simd と 要素関数 (SIMD 対応関数) を使用するケースを検討します。

「さよなら Cilk Plus」移行の手順 – 明日のためのその 3

SIMD プラグマと要素関数 (SIMD 対応関数)
SIMD プラグマ (#pragma simd) と要素関数 (Elementary Function) は、for ループとループ中の関数呼び出しを効率良くベクトル化するためにサポートされたインテル® Cilk™ Plus の言語拡張です。要素関数は OpenMP* 4.0 で declare simd による SIMD 関数がサポートされたことに合わせて、SIMD 対応関数と呼ばれるようになりました。インテル® Cilk™ Plus から他のテクノロジーへの移行において、この SIMD プラグマと要素関数が最も容易で、元のソースのセマンティクスを再現できるでしょう。それは、OpenMP* 4.0 でほぼ同等の機能が実装されているためです。

SIMD プラグマ
コンパイラーがソースコードを解析して自動ベクトル化を適用するにはいくつかの要件があります。コンパイラーは、ループのベクトル化が安全であることを確証できない限り自動ベクトル化を適用しません。そのため、コンパイラーには #pragma ivdep や #pragma vector など、開発者がコンパイラーにベクトル化を促すアノテーションが用意されています。しかし、古くからあるアノテーションでは開発者が知っているソースコードやデータ構造に関する詳細な情報をコンパイラーに伝える機能がサポートされていませんでした。#pragma vector などのアノテーションが指示されていても最終的な判断はコンパイラーに任せられます。

例えば、次のような関数とループの定義をコンパイラーが自動ベクトル化を試みる場合、

void vector_fma(float *a, float *b, float *c, int N) {

    for (int i = 0; i < N; i++)
        c[i] += a[i] * b[i];
    }

開発者は知っていてもコンパイラーは以下のことを判断できません。

  1. 変数 a、b、c がアライメントされているか?
  2. 変数 a、b と変数 c の間にエイリアシングやオーバーラップが存在しないか?
  3. ループカウント N はベクトル化を適用できるくらい大きなカウントか?

そのため比較的新しいコンパイラーは、上の課題が満たされていると想定しベクトル化したコード 1 とスカラー処理を行うコード 2 に 2 つの実行パスを生成し、このような状況に対処する傾向があります。ただし、呼び出されるたびに課題が満たされているかどうかチェックするためオーバーヘッドが生じます。開発者は、2 行目に SIMD プラグマ 「#pragma simd vectorlength(32) aligned(a,b,c:64) 」を追加することで、1 ~ 3 の要件が満たされていることを保証してコンパイラーにベクトル化を強制できます。vectorlength 節はこのデータ型が 32 要素連続してオーバーラップがなく安全にベクトル化でき、aligned 節は配列 a、b、c がそれぞれ 64 バイトにアライメントされていることを示します。このプラグマは、OpenMP* の「#pragma omp simd simdlen(32) aligned(a,b,c:64)」にそのまま置き換えることができます。表 1 に SIMD プラグマと OpenMP* SIMD の引数節の対応表を示します。#pragma simd と #pragma omp simd で使用される節の多くは同じキーワードと意味を持っています。ただし、#pragma simd の vectorlength と vectorlengthfor はそれぞれ OpenMP* の simdlen と safelen キーワードに置き換えます。

上記のコードは、/QxCORE-AVX512 (AVX512 コードを生成) オプションを指定してコンパイルすると、vectorlength(32) が指定されているため 512 ビット幅のベクトル化の安全性が保障され、ZMM レジスターを使用したインテル® AVX-512 コードを生成されます。vectorlength(32) がなかったり、引数が 32 未満であると 512 ビット幅の安全性と効率が不明であるため、コンパイラーは XMM/YMM を使用した 256 ビットモードのコードを生成する傾向があります。

表 1 #pragma simd と #pragma omp sind の節の対応表

  #pragma simd
(インテル® Cilk™ Plus)
#pragma omp simd
(OpenMP*)
安全なベクトル長を指示します。
n = 2,4,8,16,32,64 のいずれかです。
vectorlength(n) simdlen(n)
基本データ型を指定します。 vectorlengthfor(データタイプ) safelen(n)
変数のアライメントを指定します。 aligned(変数:アライメント) aligned(変数:アライメント)
ループのそれぞれの反復に対して
各変数がプライベートになります。
private private
SIMD ループに入る前に、各反復の
すべてのプライベート・インスタンスに
初期値がブロードキャストされます。
firstprivate firstprivate
SIMD ループ終了時の各変数の値は、
その SIMD ループシーケンスの最後の
反復の結果になります
lastprivate lastprivate
変数に対するステップ増分を指示します。 linear(変数:ステップ) linear(変数:ステップ)
変数のリダクション演算を指示します。 reduction(演算子:変数) reduction(演算子:変数)
ベクトル化に失敗したときに、
報告するかどうかをコンパイラーに
指示します。
assert なし
余剰ループをベクトル化するかどうかを
コンパイラーに指示します。
[no]vecremainder なし

前述の関数とループ構造の派生として次のようなケースも考えられます。この場合、c += a[] * b[] の演算結果を戻り値として返します。c は配列ではなく単一の float 型の変数として定義されリダクション演算が行われています。このケースでは前述の #pragma simd に reduction(+:c) 節を追加することでループのベクトル化と変数 c への + のリダクションを明示できます。これも OpenMP* のリダクションと同等の構文と機能を持つためそのまま OpenMP* ディレクティブの節としてそのまま移行できます。

float vector_fma(float *a, float *b, int N) {
    float c;
    #pragma simd vectorlength(32) aligned(a,b:64) reduction(+:c)      // Cilk Plus
    // #pragma omp simd simdlen(32) aligned(a,b,c:64) reduction(+:c)  // OpenMP
    for (int i = 0; i < N; i++)
        c += a[i] * b[i];
    return c;
}

/Qopt-report:[1-5] /Qopt-report-phase:vec (-qopt-report=[1-5] –qopt-report-phase=vec [Linux*]) オプションを指定してコンパイルするとベクトル化がどのように適用されたかを示すレポートファイルを生成できます。また、インテル® Advisor を使用して実行することで実際のベクトル化の効果を確認できます。

要素関数 (Elementary Function)
ループ中の関数呼び出しは、ループのベクトル化を妨げる要因の 1 つです。関数呼び出しでは、伝統的にスカラー変数を渡し、スカラーの戻り値を返します。関数を SIMD 対応とすることで、ベクトル引数を受け取り、ベクトル値を返すことができます。ベクトル化されたループからこの関数を呼び出すことが可能になります。要素関数を定義するには、Windows* では関数定義の前に __declspec(vector)、Linux* では __attribute__(vector) を追加します。これにより、コンパイラーはベクトルデータを処理する関数を生成できるようになります。次のコードを考えてみます。

__declspec(vector) float min(float a, float b) { //__attribute__(vector) ― Linux*
    return a < b ? a : b;
}
__declspec(vector)) float distsq(float x, float y) { //__attribute__(vector) ― Linux*	
    return (x - y) * (x - y);
}
void example() {
    #pragma simd	
    for (i = 0; i < N; i++) {		
        d[i] = min(distsq(a[i], b[i]), c[i]);	
    }
}

ここでは、関数 example() 中の for ループで、関数 min() と distsq() を入れ子に呼び出しています。min と distsq が要素関数として定義されていないと、コンパイラーはスカラーデータを処理する関数呼び出しのループ構造を生成します。2 つの関数を要素関数として定義することで、ベクトルデータを処理する関数が生成され、for ループをベクトル化できるようになります。9 ~ 10 行目の for ループは、インテル® Cilk™ Plus の配列要素を使用して d[:] = min(distsq(a[:], b[:]), c[:]); と記述することもできます。要素関数を使用した上記のコードは、OpenMP* 4.0 以降の SIMD 対応関数 (#pragma omp declare simd) を使用して次のように書き換えることができます。表 2 にインテル® Cilk™ Plus の要素関数と OpenMP* の SIMD 対応関数のオプション節の対応一覧を示します。OpenMP* の SIMD 対応関数には表に示す以外のオプションも用意されており今後も機能が追加されていく予定です。

#pragma omp declare simd
float min(float a, float b) {	
    return a < b ? a : b;
}
#pragma omp declare simd
float distsq(float x, float y) {	
    return (x - y) * (x - y);
}
void example() {
    #pragma omp parallel for simd	
    for (i = 0; i < N; i++) {		
        d[i] = min(distsq(a[i], b[i]), c[i]);	
    }
}

表 2 インテル® Cilk™ Plus の要素関数と OpenMP* の SIMD 対応関数の節の対応表

  __declspec(vector(節))
(インテル® Cilk™ Plus)
#pragma omp declare
simd (節)
(OpenMP*)
指定されたプロセッサーに最適な命令、
呼び出し元/呼び出し先インターフェイスを生成
processor(cpuid) コンパイラーの機能を使用
安全なベクトル長を指示します。
n = 2,4,8,16,32,64 のいずれかです。
vectorlength(n) simdlen(n)
変数に対するステップ増分を指示します。 linear(引数) linear(引数)
指定された引数の値をすべての反復に
ブロードキャストするようにコンパイラー
に指示します。
uniform(引数) uniform(引数)
ルーチンのマスク付きまたはマスクなしの
ベクトルバージョンを生成するように
コンパイラーに指示します。
mask/nomask コンパイラーの機能を使用

ここでは、インテル® Cilk™ Plus の SIMD プラグマと要素関数を OpenMP* に移行するほんの一例を紹介しました。さまざまな用例が考えられますが、ほとんどのケースでは比較容易に移行できると考えています。インテル® Cilk™ Plus の詳細については、iSUS の関連記事、インテル® コンパイラーのマニュアル、または書籍をご覧ください。

最後に
最後までお読みいただきありがとうございます。また、長年のインテル® Cilk™ Plus のご利用、開発チームに代わってお礼申し上げます。今後ともインテル® コンパイラーを活用いただきますことをお願いして、この記事を終えたいと思います。

その他の記事

第 1 回「Cilk がやってきた」から「さよなら Cilk Plus」
第 2 回「さよなら Cilk Plus」インテル® Cilk™ Plus の 3 つのキーワードを使用するケース
第 3 回「さよなら Cilk Plus」インテル® Cilk™ Plus の配列表記 (Array Section) とヘルパー関数を使用するケース
第 4 回「さよなら Cilk Plus」インテル® Cilk™ Plus の #pragma simd と要素関数 (SIMD 対応関数) を使用するケース

参考文献

  • https://software.intel.com/en-us/articles/migrate-your-application-to-use-openmp-or-intelr-tbb-instead-of-intelr-cilktm-plus
    https://software.intel.com/en-us/articles/migrate-your-application-to-use-openmp-or-intelr-tbb-instead-of-intelr-cilktm-plus

上記の記事の翻訳版を以下に用意しましたので参照ください。

関連記事