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

マルチコア・プラットフォーム向けにループのさらなる並列化

マルチコア・プラットフォーム向けのループの並列化は特定の条件を前提としています。コンパイラーでループを並列化するには、次の 3 つの要件が満たされなければなりません。

反復が実行される順序に論理的に依存してはなりません。例えば、同じデータが異なる順で追加された場合、蓄積される丸め誤差の変分はわずかです。配列を合計するようないくつかのケースでは、コンパイラーは単純な変換を行うことで明らかな依存性を排除できることがあります。

また、ポインターや配列参照の潜在的なエイリアスも、安全な並列化にとっては一般的に知られている障害です。2 つのポインターが同じメモリーの場所を指す場合、両方のエイリアスが作成されます。コンパイラーは、2 つのポインターまたは配列参照が同じメモリーの場所を指しているかどうかを判断できません。例えば、関数の引数、ランタイムデータ、または複雑な計算の結果に依存する場合、

コンパイラーは、ポインターあるいは配列参照が安全なことを証明できなければ、ループを並列化しません (ただし、ランタイム時にエイリアスを明示的にテストするための代替コードパスの生成が有益であると考えられる場合を除きます)。

C でポインターがエイリアスされないことを表明するには、ポインター宣言で restrict キーワードを使用して、[Q]restrict コマンドライン・オプションを指定します。コンパイラーは、安全が確保されないループを並列化することはありません。

特定のループの並列化が安全で潜在的なエイリアスが無視できることが判明していれば、#pragma parallel プラグマを使用してループを並列化するようにコンパイラーに指示できます。

繰り返し間の依存関係を持つループの並列化

ループの自動並列化の前に、コンパイラーは、並列化を阻む繰り返し間の依存関係がループにないことを証明しなければなりません。ループのある反復でメモリー位置が書き込まれ、ループの別の反復でその場所がアクセス (読み取り/書き込み) される場合、繰り返し間の依存関係が存在します。繰り返し間の依存関係は、a(1:100) から読み取ったり、a(0:99) へ書き込んだりするループのように、重複する配列範囲へアクセスするループでよく発生します。

ループに繰り返し間の依存関係がなくても、それを証明するのに十分な情報がない場合、コンパイラーはそのループを並列化しないこともあります。そのような場合、#pragma parallel プラグマを使用してループに関する追加情報をコンパイラーに提供することができます。for ループの前に #pragma parallel プラグマを追加して、ループに繰り返し間の依存関係がないことをコンパイラーに知らせます。自動並列化の解析では、依存関係の可能性が無視されます。しかし、それでも並列化がループのパフォーマンスを向上する見込みが低いと判断した場合、コンパイラーはループを並列化しないこともあります。

#pragma parallel always プラグマは、#pragma parallel プラグマと同じように依存性の可能性を無視する効果があります。ただし、ループの並列化によるパフォーマンス向上の可能性を推定するコンパイラーのヒューリスティックも無効にします。これにより、並列化がパフォーマンスを向上しないと推定された場合でも、ループが並列化されることがあります。

#pragma noparallel プラグマは、for ループ直後の自動並列化を抑止します。ヒントである #pragma parallel とは異なり、noparallel プラグマはループ直後の並列化の抑止を保証します。

これらのプラグマは、自動並列化が [Q]parallel オプションで有効にされている場合にのみ効果があります。

private 節によるループの並列化

ガイド付き自動並列化ツールを使用すると、コンパイラーの自動並列化は、並列化を促進するためにプログラムのどこを変更するべきか、アドバイスを提供します。例えば、条件が真であるかをチェックして、真であれば、#pragma parallel をコードに挿入するようアドバイスします。これにより、関連するループが再コンパイル時に並列化されます。

各スレッドによる変数の新しいプライベート・コピー (ほかのスレッドからは不可視) の作成が有効であることを示し、ループのオリジナル変数を新規のプライベート変数に置換するには、#pragma parallel プラグマを private 節とともに使用します。private 節では、スカラーおよび配列型変数をリストでき、プライベート化する配列要素の数を指定することが可能です。

並列ループに入る前にオリジナルの値でプライベート変数を初期化する必要がある場合は、firstprivate 節を使用してその変数を指定します。

並列化されたループの終了後も変数の値を再利用したい場合は、lastprivate 節を使用してその変数を指定します。lastprivate 節でプライベート化された特定の変数を指定すると、並列化されたループの終了後、その値はオリジナルの変数にコピーされます。

同一ループに対して privatelastprivate 節で同じ変数を使用しないでください。エラーメッセージが出力されます。

外部関数の呼び出しを持つ並列化のループ

コンパイラーは、相対的に単純な構造のループのみを効率的に解析できます。例えば、コンパイラーは関数呼び出しに依存性をもたらす副作用があるかどうか分からないため、外部関数の呼び出しを含むループのスレッド安全性を判断できません。[Q]ipo オプションを使用して、プロシージャー間の最適化を行うことができます。このオプションを使用して、コンパイラーは呼び出された関数の副作用を解析できます。

OpenMP* による並列化のループ

並列化が可能であることが分かっており、コンパイラーが自動的にループを並列化できない場合、OpenMP* を使用してください。コンパイラーよりも開発者のほうがコードを理解しており、より粗い粒度で並列化を表現できるため、OpenMP* が推奨されます。行列乗算内などの入れ子構造のループには、自動並列化が有効です。適度な粗粒度の並列化は、外部ループのスレッド化に起因し、ベクトル化やソフトウェア・パイプラインを使用して内部ループをより細かい粒度の並列化に最適化できるようにします。

ループを並列化するしきい値引数

ループが並列化できる場合でも、必ずしも並列化すべきであるとは限りません。コンパイラーは、しきい値引数を使用して、ループを並列化するかどうかを決定します。[Q]par-threshold コンパイラー・オプションは、この動作を調整します。しきい値の範囲は 0 から 100 です。0 は、安全なループを常に並列化するようにコンパイラーに指示します。100 は、パフォーマンスの向上が期待できるループのみを並列化するように指示します。ループが並列化されたかどうかを判断するには、[Q]par-report オプションを使用します。コンパイラーは、並列化できなかったループもレポートし、その理由を示します。これらのコンパイラー・オプションについての詳細は、「OpenMP* オプションおよび並列処理オプション」を参照してください。

次に、これらのオプションを組み合わせて使用する例を示します。

void add (int k, float *a, float *b) {
  for (int i = 1; i < 10000; i++) {
   a[i] = a[i+k] + b[i];
  } 
}

コマンドラインで以下のコンパイラー・コマンドを入力します。

//Linux* および macOS*
icpc -c -parallel -qopt-report-phase=par -qopt-report=3 add.cpp

コンパイラーは、次のような結果を出力します。

サンプル結果

add.cpp 
procedure: 
add serial loop: line 2 
anti data dependence assumed from line 2 to line 2, due to "a" 
flow data dependence assumed from line 2 to line 2, due to "a" 
flow data dependence assumed from line 2 to line 2, due to "a"

コンパイラーは k の値が分からないため、例えば、k が -1 の場合でも、反復は互いに依存すると仮定します。#pragma parallelプラグマを挿入して、コンパイラーの仮定を無効にすることができます。

void add(int k, float *a, float *b) {
  #pragma parallel
  for (int i = 0; i < 10000; i++) {
    a[i] = a[i+k] + b[i];
  }  }

注意

k の値が 10000 未満の場合は、この関数を呼び出さないでください。10000 未満の値を渡すと、正しくない結果を引き起こす場合があります。

関連情報