マルチスレッド開発ガイド: 4.6 インテル® Parallel Composer を利用して並列コードを開発する

インテル® Parallel Studio XE特集

この記事は、インテル® ソフトウェア・ネットワークに掲載されている「Curing Thread Imbalance Using Intel® Parallel Amplifier」 (http://software.intel.com/en-us/articles/curing-thread-imbalance-using-intel-parallel-amplifier/) の日本語参考訳です。


編集注記:
本記事は、2011 年 5 月 4 日に公開されたものを、加筆・修正したものです。

英語版のサイトではこの記事は公開が停止されています。インテル® Parallel Composer という製品が廃止されたため利用できない記事であると判断されたようです。インテル® Parallel Composer の後継は、インテル® Parallel Studio XE の Composer Edition であり、最新のコンパイラーでも役立つ内容があることから日本語版では更新して公開を続けています。

コードの並列化にはさまざまな手法があります。

この記事は、「マルチスレッド・アプリケーションの開発のためのガイド」の一部で、インテル® プラットフォーム向けにマルチスレッド・アプリケーションを効率的に開発するための手法について説明します。

はじめに

インテル® コンパイラーには並列実行できるコード構造を自動的に検出して最適化するいくつかの方法 (自動ベクトル化や自動並列化など) が用意されていますが、多くの場合コードの変更が必要です。挿入するプラグマや関数は、並列スレッドの分割やスケジュール実行を実際に行うランタイム・ライブラリー (インテル® スレッディング・ビルディング・ブロック (インテル® TBB)、OpenMP*、Win32* API など) に依存します。各手法の主な違いは、実行に際して提供される制御のレベルです。一般に、より多くの制御が提供されるほど、より多くのコードの変更が必要になります。

インテル® Cilk™ Plus (この機能はサポートが終了しています)

インテル® C++ コンパイラーに含まれるインテル® Cilk™ Plus 言語拡張を使用することで、C/C++ プログラムに細粒度のタスクを実装し、新規および既存のソフトウェアを簡単に並列化して、効率良くマルチプロセッサーを活用できます。インテル® Cilk™ Plus には、次の主要な機能があります。

  • キーワードセット (_Cilk_spawn、_Cilk_sync、_Cilk_for) – タスクの並列化を表現します。
  • レデューサー – 各タスクに対して自動で共有変数のビューを作成し、タスクの完了後に共有変数に戻すことによって、タスク間の共有変数の競合を排除します。
  • 配列表記 (アレイ・ノーテーション) – 配列やスカラーの全体または一部に適用される、関数や演算全体のデータ並列化を有効にします。
  • simd プラグマ – インテル® コンパイラーで標準規格に準拠する C/C++ コードを記述しながら、ハードウェア SIMD 並列化を活用するベクトル並列化を表現します。
  • 要素関数 – スカラー引数または配列要素で並列に呼び出すことができます。要素関数は、関数のシグネチャーの前に 「__declspec(vector)」 (Windows* の場合) や 「__attribute_((vector))」 (Linux* の場合) を追加して定義します。
cilk キーワード/プラグマ 説明
cilk spawn (キーワード) 関数呼び出し文を変更し、関数の呼び出し元 (親) と、呼び出した関数 (子) を並列に実行できること (ただし、必ずしも並列に実行する必要はないこと) をランタイムシステムに通知します。「cilk spawn」キーワードを利用するには #include が必要です。
cilk sync (キーワード) この記述を含む関数は、スポーンした子タスクが完了するまで現在の位置で待機することを指示します。「cilk_sync」キーワードを利用するには #include が必要です。
cilk for (キーワード) 指定した通常の C/C++ の for ループを置き換えて、ループの各反復を並列に処理することを指示します。この文は、ループを 1 反復以上のチャンクに分割します。各チャンク自身はシリアル実行され、ループが実行される間、各チャンクをスポーンすることで並列実行します。「cilk_for」キーワードを利用するには #include が必要です。
cilk grainsize (プラグマ) cilk_for ループでチャンクに割り当てるループの反復回数 (粒度) を指定します。
CILK_NWORKERS (環境変数) ワーカースレッドの数を指定します。

例: cilk spawn、cilk sync

以下の例では、cilk_spawn はスポーンしたり、新しいスレッドを作成するわけではありません。インテル® Cilk™ Plus ランタイムに、空いているワーカースレッドが fib(n-1) への呼び出しに続くコードをスチールし、関数呼び出しと並行に実行できることを示します。

CilkNospawn-Nosync.png Cilk-Arrow.png cilkspwan-sync.png
例: cilk for、cilk レデューサー

以下の例では、cilk_for はループ本体のコードの複数のインスタンスを実行コアにスポーンし、並列に実行します。レデューサーは、データ競合を回避し、ロックを使用せずにリダクション操作を行うことができます。

Cilk-noForNoReducer.png Cilk-Arrow.png Cilkfor-reducer.png
例: 配列表記 (アレイ・ノーテーション)

以下の例では、C/C++ の通常のインデックス構文を、同じ結果を生成する部分配列記述子に置換します。ここで、配列表記と標準の C/C++ ループを使用する違いは、暗黙のシリアル順序がないことです。そのため、コンパイラーはコード生成でベクトル並列化を行うことが予想されます。つまり、SSE 命令を使用して、SIMD 形式の加算を実装します。コンパイラーは、配列操作に対して言語のすべてのビルトイン演算子 (‘+’、’*’、’&’、’&&’、など) を含む、ベクトル SIMD コードを生成します。

Cilk-NoarrayNotation.png Cilk-Arrow.png CilkArrayNotation.png
例: pragma simd

#pragma simd を使用するベクトル化は、コンパイラーにループをベクトル化するように指示します。コードのベクトル化に必要なソースコードの変更は、最小限に抑えるように設計されています。simd プラグマを使用すると、「#pragma vector always」や「#pragma ivdep」などのベクトル化のヒントを利用しても、コンパイラーが自動ベクトル化しないループをベクトル化できます。

char foo(char *A, int n){
int i;
char x = 0;
#ifdef SIMD
#pragma simd reduction(+:x)
#endif
#ifdef IVDEP
#pragma ivdep
#endif
  for (i=0; i<n; i++){
    x = x + A[i];
  }
  return x;
}

以下は、さまざまなコンパイラー・オプションを使用した上記のコードのコンパイル例です。コンパイル結果は、SIMD プラグマが単純なループのベクトル化にどのような影響を与えるかを示しています。

>icl /c /Qvec-report2 simd.cpp
simd.cpp
simd.cpp(12) (col. 3): リマーク: ループはベクトル化されませんでした: ベクトル依存関係が存在しています。
>icl /c /Qvec-report2 simd.cpp /DIVDEP
simd.cpp
simd.cpp(12) (col. 3): リマーク: ループはベクトル化されませんでした: ベクトル依存関係が存在しています。
>icl /c /Qvec-report2 simd.cpp /DSIMD
simd.cpp
simd.cpp(12) (col. 3): リマーク: "simd" ループがベクトル化されました。

例: 要素関数

__declspec(vector)
int vfun_add_one(int x)
{
  return x+1;
}

以下は、上記のコードのコンパイルとその結果です。

>icl /c /Qvec-report2 elementalfunc.cpp
elementalfunc.cpp
elementalfunc.cpp(3) (col. 1): リマーク: 関数がベクトル化されました。

インテル® スレッディング・ビルディング・ブロック (インテル® TBB)

インテル® TBB は、C++ プログラムを並列化する豊富な手法を提供する、マルチコア・プロセッサーのパフォーマンスの活用に役立つライブラリーです。プラットフォームの詳細を抽象化する、より高いレベルのタスクベースの並列化と、パフォーマンスとスケーラビリティーのためのスレッド化のメカニズムを示します。オブジェクト指向や C++ の汎用フレームワークにも適合します。インテル® TBB は、ランタイムベースのプログラミング・モデルを使用し、開発者に標準テンプレート・ライブラリー (STL) と同じようなテンプレート・ライブラリーをベースとした汎用並列アルゴリズムを提供します。

例:

#include "tbb/task_scheduler_init.h"
#include "tbb/blocked_range.h"
#include "tbb/parallel_for.h"
#include <vector>
void foo() {
  tbb::task_scheduler_init init;
  size_t length = 1000000;
  std::vector<float> a(length, 2), b(length, 3), c(length, 0);
  tbb::parallel_for(tbb::blocked_range<size_t>(0, length),
  [&](const tbb::blocked_range<size_t> &r){
  for (size_t i=r.begin(); i< r.end(); i++)
   c[i] = a[i] + b[i];
  },
     tbb::auto_partitioner());
}

インテル® TBB タスク・スケジューラーはロードバランスを自動的に行うため、開発者が複雑なタスク実行を制御する必要はありません。プログラムを小さなタスクに分割することによって、インテル® TBB スケジューラーは処理が均等に分散されるようにタスクをスレッドに割り当てます。

インテル® C++ コンパイラーとインテル® TBB はどちらも、新しい C++0x ラムダ関数をサポートしています。これにより、STL とインテル® TBB のアルゴリズムがより使いやすくなります。ラムダ式を使用するには、/Qstd=c++0X (C++11) コンパイラー・オプションを指定してコードをコンパイルしてください。

インテル® Array Building Blocks (インテル® ArBB) (この機能はサポートが終了しています)

インテル® ArBB は色々な定義ができます。ライブラリーに裏付けられた API であり、特別なプリプロセッサーを必要としません。インテル® ArBB は、プログラミング言語拡張 (つまり、ホスト言語を必要とする「補足言語」です) であり、不規則な行列や疎行列などの複雑なデータの並列化に対応するよう C++ を拡張します。次のような特性があります。

  • 移植可能な並列開発プラットフォーム
  • ハードウェアに依存しない並列計算
  • シーケンシャル・セマンティクス、優れたデータ局所性
  • デフォルトでの安全性: デッドロックなし、データ競合なし

インテル® ArBB は、計算を多用するデータ並列アプリケーション (ベクトル算術演算などがしばしばかかわる) に最適です。この API は、汎用データ並列プログラミング・ソリューションをもたらします。アプリケーション開発者は、特定のハードウェア・アーキテクチャーへの依存から解放され、既存の C++ 開発ツールと統合し、並列アルゴリズムを高レベルで指定できます。インテル® ArBB は、モジュール化によるオーバーヘッドを排除できる動的コンパイルを基にしています。計算処理の高レベルな記述を効率良い並列実装に変換することで、SIMD とスレッドレベルの並列化の双方をうまく利用することができます。

インテル® ArBB の主な機能

すべての ArBB プログラムは 2 回コンパイルされます。1 回目はインテル® アーキテクチャー (IA) のバイナリー配布用の C++ コンパイルです。2 回目はハイパフォーマンス実行のための動的コンパイルで、インテル® ArBB の動的エンジンによって行われます。データが複数のコア向けに最適化されるよう、データを C++ 空間からインテル® ArBB 空間にコピーする必要があります。データは隔離されたデータ空間に保たれ、コレクション・クラスによって管理されます。

演算

  • 計算処理は、インテル® ArBB のコレクション型、スカラー型に対する演算を使用して、C++ 関数として指定します。
  • コンポーネント単位の操作と集合操作の両方がサポートされています。
  • 計算処理は、次の方法で実行されます。
    • マップ: 配列のすべての要素に対して並列に関数を実行します。
    • 呼び出し: 一連の (並列) 操作を実行します。

OpenMP* を使用した並列化

OpenMP* は、移植性に優れたマルチスレッド・アプリケーション開発のための業界標準規格です。インテル® C++ コンパイラー 12.1 は、OpenMP* C/C++ バージョン 3.1 の仕様をサポートしています。最新のインテル® C++/Fortran コンパイラー 19.1 は、OpenMP* 5.0 の一部の機能までをサポートしています。詳細は、OpenMP* Web サイト (http://www.openmp.org/ (英語)) を参照してください。OpenMP* を使用した並列化は、開発者が OpenMP* ディレクティブを使用して制御します。このアプローチは細粒度 (ループレベル) から粗粒度 (関数レベル) のスレッド化に効果的です。

OpenMP* ディレクティブは、シリアル・アプリケーションを並列アプリケーションに変換する簡単でパワフルな方法を提供し、マルチコアシステムの並列実行から大きなパフォーマンス・ゲインを引き出す可能性をもたらします。OpenMP* ディレクティブは、/Qopenmp コンパイラー・オプションを指定すると有効になります。このコンパイラー・オプションを指定しなかった場合、ディレクティブは無視されます。つまり、同じソースコードからアプリケーションのシリアルバージョンと並列バージョンの両方をビルドできます。共有メモリー並列コンピューターでは、シリアル実行と並列実行の単純比較ができます。

次の表に、一般的に使用される OpenMP* ディレクティブを示します。

ディレクティブ 説明
#pragma omp parallel for [節] … for ループ プラグマ直後のループを並列化します。
#pragma omp parallel sections [節] … { [#pragma omp section structured-block] … } 並列チームのスレッドに異なるセクションの実行を分配します。各構造ブロックは、チームの 1 つのスレッドにより、その暗黙のタスクのコンテキスト内で一度実行されます。
#pragma omp master 構造化ブロック マスター構造内に含まれるコードをスレッドチームのマスタースレッドで実行します。
#pragma omp critical [ (名前) ] 構造化ブロック 構造ブロックへの排他制御アクセスを提供します。プログラムの任意の場所で、一度に 1 つのクリティカル・セクションのみ実行できます。
#pragma omp barrier 並列領域内の複数のスレッドの実行を同期させます。バリアーの前のすべてのコードが全スレッドで完了するまで待機します。同期が完了するまでどのスレッドも barrier ディレクティブの後のコードは実行しません。
#pragma omp atomic 簡単な式 ハードウェア同期プリミティブによる排他制御を提供します。クリティカル・セクションはコードのブロックに対する排他制御アクセスを提供しますが、atomic ディレクティブは単一のステートメントに対する排他アクセスを提供します。
#pragma omp threadprivate (リスト) スレッドごとに 1 つのインスタンスに複製する (つまり、各スレッドは変数の個々のコピーで動作) グローバル変数のリストを指定します。

例:

void sp_1a(float a[], float b[], int n) {
  int i;
  #pragma omp parallel shared(a,b,n) private(i)
  {
    #pragma omp for
    for (i = 0; i < n; i++)
      a[i] = 1.0 / a[i];
      #pragma omp single
      a[0] = a[0] * 10;
      #pragma omp for nowait
    for (i = 0; i < n; i++)
      b[i] = b[i] / a[i];
  }
}

以下は、上記のコードのコンパイルとその結果です。

icl /c /Qopenmp /Qopenmp-report par1.cpp
par2.cpp(5): (col. 5) リマーク: OpenMP* 定義ループが並列化されました。
par2.cpp(10): (col. 5) リマーク: OpenMP* 定義ループが並列化されました。
par2.cpp(3): (col. 3) リマーク: OpenMP* 定義領域が並列化されました。

/Qopenmp-report[n] (n は 0 から 2) コンパイラー・オプションは、OpenMP* パラレライザーの診断メッセージのレベルを制御します (最新のコンパイラーでは、/Qopt-report[n] /Qopt-report-phase:openmp を使用します)。このオプションを使用するには、/Qopenmp オプションを指定する必要があります。n を指定しない場合、デフォルトの /Qopenmp-report1 が使用され、正常に並列化されたループ、領域、セクションを示す診断メッセージが表示されます。

コードにはディレクティブのみが挿入されるため、インクリメンタルにコードを変更することができます。インクリメンタルなコードの変更は、シリアルバージョンの一貫性の維持に役立ちます。コードを 1 つのプロセッサー上で実行すると、変更前のソースコードを実行したときと同じ結果が得られます。OpenMP* は、複数のプラットフォームとオペレーティング・システムをサポートするシングル・ソースコード・ソリューションです。また、OpenMP* ランタイムにより適切なコア数が自動的に選択されるため、コア数を特定する必要はありません。

OpenMP* バージョン 3.0 では、OpenMP* が最もよく使用されるループレベルの並列化に加え、新しくタスクレベルの並列化構造が追加され、関数の並列化が容易になりました。タスクモデルでは、効率的に並列化することが困難な、再帰などの不規則なパターンの動的データ構造や複雑な制御構造を含むプログラムを並列化できます。

task プラグマは並列領域のコンテキスト内で動作し、明示的なタスクを作成します。並列領域内に task プラグマが存在すると、タスクブロックの内側のコードは、概念的には並列領域を実行するスレッドのうちの 1 つによって実行されるようにキューイングされます。シーケンシャルなセマンティクスを保持するために、並列領域内でキューイングされているすべてのタスクは並列領域の最後までに完了します。開発者は、明示的なタスク間、および明示的なタスクの内側と外側のコード間で依存性が存在しないこと、または適切に同期されることを確認する必要があります。OpenMP* バージョン 3.1 の新機能は、こちらを参照してください。OpenMP バージョン 5.0 の機能については、こちらを参照してください。

例:

#pragma omp parallel
#pragma omp single
{
  for(int i = 0; i < size; i++)
  {
    #pragma omp task
    setQueen (new int[size], 0, i, myid);
  }
}

Win32* スレッド API と Pthreads*

場合によっては、ネイティブスレッド API の柔軟性を利用したいこともあるでしょう。この手法の主な利点は、この記事でこれまでに説明した抽象化手法よりも、スレッドをより柔軟かつ詳細に制御できることです。ただし、他の手法ではランタイムシステムが制御する生成、スケジューリング、同期、ローカルストレージ、ロードバランス、破棄などのスレッド実装作業をこの手法ではすべて開発者が行う必要があるため、実装に必要なコード量は多くなります。さらに、正しい数のスレッドを作成するために、利用可能なコア数を特定する必要があります。特にこの作業は、プラットフォームに依存しないソリューションでは非常に複雑になります。

例:

void run_threaded_loop (int num_thr, size_t size, int _queens[])
{
  HANDLE* threads = new HANDLE[num_thr];
  thr_params* params = new thr_params[num_thr];
  for (int i = 0; i < num_thr; ++i)
  {
    // 各スレッドに割り当てる行数を同じにします
    params[i].start = i * (size / num_thr);
    params[i].end = params[i].start + (size / num_thr);
    params[i].queens = _queens;
    // 各スレッドの引数に異なるメモリーへの
    // ポインターを設定してデータ競合を回避します
    threads[i] = CreateThread (NULL, 0, run_solve,
      static_cast<void *> (&params[i]), 0, NULL);
  }
  // すべてのスレッドが完了するまで待機してスレッドを結合します
  WaitForMultipleObjects (num_thr, threads, true, INFINITE);
  //  メモリーを解放します
  delete[] params;
  delete[] threads;
}

マルチスレッド・ライブラリー

アプリケーションに並列化を実装する別の方法は、インテル® マス・カーネル・ライブラリー (インテル® MKL) やインテル® インテグレーテッド・パフォーマンス・プリミティブ (インテル® IPP) などのマルチスレッド・ライブラリーを使用することです。

インテル® MKL は、スレッド化に OpenMP* を使用して、パフォーマンスを最大限に引き出すように高度に最適化されたスレッド化演算ルーチンを提供します。スレッド化されたインテル® MKL 関数を利用するには、OMP_NUM_THREADS 環境変数に 2 以上の値を設定して指定するだけです。インテル® MKL には、シリアルと並列のどちらで計算を実行するかを決定する内部的なしきい値があります。開発者は OpenMP* API の omp_set_num_threads 関数を使用してしきい値を手動で設定することもできます。インテル® MKL の並列化については、インテル® MKL Windows 版のオンライン・テクニカル・ノート やインテル® MKL 10.x のスレッド化に関する別の記事 (http://software.intel.com/en-us/articles/intel-math-kernel-library-intel-mkl-intel-mkl-100-threading/) も参照してください。

インテル® IPP は、マルチメディア、データ処理、通信アプリケーション向けに高度に最適化された、ソフトウェア関数の広範囲なマルチコア対応ライブラリーです。インテル® IPP も、スレッド化に OpenMP* を使用しています。インテル® IPP のスレッド化と OpenMP* のサポートについては、別の記事 (http://www.intel.com/support/performancetools/libraries/ipp/sb/CS-026584.htm (英語)) も参照してください。

インテル® C++ コンパイラーも、数学演算と超越演算のデータ並列パフォーマンスにはインテル® IPP を使用して STL valarray を実装しています。C++ の valarray テンプレートには、ハイパフォーマンス・コンピューティング向けの配列演算が含まれています。これらの演算は、ベクトル化などの低レベルのハードウェア機能を活用するように設計されています。インテル® C++ コンパイラーの valarray は、最適化された代替の valarray ヘッダーファイルを使用して、インテル® IPP 最適化バージョンの valarray にリンクするように実装されているので、ソースコードを変更する必要はありません。インテルのパフォーマンス最適化ヘッダーファイルを使用して valarray ループを最適化するには、/Quse-intel-optimized-headers コンパイラー・オプションを指定します。

自動並列化

自動並列化はインテル® C++ コンパイラーの強力な機能です。自動並列化では、コンパイラーはプログラムの本来の並列性を自動的に検出します。自動パラレライザーは、アプリケーション・ソースコード中のループのデータフローを解析して、安全かつ効率的に並列実行可能なマルチスレッド・コードを生成します。データの依存性が存在する場合、ループを自動並列化するためにはループを再構成する必要があります。

自動並列化では、並列化に関するすべての判断はコンパイラーによって行われ、開発者が並列化するループを制御することはできません。自動並列化を OpenMP* と組み合わせると、より高いパフォーマンスが得られます。OpenMP* と自動並列化を組み合わせた場合、OpenMP* ディレクティブを含むループの並列化には OpenMP* が、OpenMP* 以外のループの並列化には自動並列化がそれぞれ使用されます。自動並列化を有効にするには、/Qparallel コンパイラー・オプションを指定してください。

例:

#define N 10000
float a[N], b[N], c[N];
void f1() {
  for (int i = 1; i < N; i++)
   c[i] = a[i] + b[i];
 }

以下は、上記のコードのコンパイルとその結果です。

> icl /c /Qparallel /Qpar-report par1.cpp
par1.cpp(5): (col. 4) リマーク: ループが自動並列化されました。

/Qpar-report (最新のコンパイラーでは、/Qopt-report[n] /Qopt-report-phase:par を使用) のデフォルトでは、自動パラレライザーは自動並列化されたループを表示します。/Qpar-report[n] オプション (n は 0 から 3) を指定すると、自動パラレライザーは自動並列化されたループと、自動並列化に失敗したループに関する診断メッセージを表示します。例えば、/Qpar-report3 を指定すると、正常に自動並列化されたループと自動並列化に失敗したループの診断メッセージに加えて、自動並列化を妨げると判明/想定した依存関係に関する追加情報を表示します。この診断情報は、自動並列化するループを再構成するときに役立ちます。

自動ベクトル化

ベクトル化は、インテル® プロセッサー上でループのパフォーマンスを最適化する手法です。ベクトル化で定義される並列化は、プロセッサーの SIMD (英語) ハードウェアで実現可能なベクトルレベルの並列処理 (VLP) に基づきます。インテル® C++ コンパイラーの自動ベクトライザーは、並列に実行できるプログラム内の低レベルの演算を検出して、1 つの演算で 1、2、4、8、16 または 32 バイト (将来のプロセッサーでは 64 バイトに拡張) のデータ要素を処理するようにシーケンシャル・コードを変換します。

コンパイラーで自動的にベクトル化するには、ループは独立している必要があります。自動ベクトル化は、前述した自動並列化や OpenMP* などの他のスレッドレベルの並列化手法と組み合わせて使用できます。ほとんどの浮動小数点アプリケーションと一部の整数アプリケーションは、ベクトル化によってパフォーマンスが向上します。デフォルトのベクトル化レベルは /arch:SSE2 で、インテル® ストリーミング SIMD 拡張命令 2 (インテル® SSE2) 向けのコードを生成します。デフォルト以外のターゲットで自動ベクトル化を有効にするには、/arch (例: /arch:SSE4.1) または /Qx (例: /QxSSE4.2, QxHost) コンパイラー・オプションを指定してください。

下記の図の左は、ベクトル化なしでループの反復をシリアル実行しているため、SIMD レジスターの多くが使用されていません。図の右は、ベクトル化によりループの各反復について配列の 4 つの要素が並列に実行され、SIMD レジスターがすべて使用されています。


図 1.ループの反復とベクトル化

例:

#define N 10000
float a[N], b[N], c[N];
void f1() {
  for (int i = 1; i < N; i++)
   c[i] = a[i] + b[i];
 }

以下は、上記のコードのコンパイルとその結果です。

> icl /c /QxSSE4.2 /Qvec-report par1.cpp
par1.cpp(5): (col. 4) リマーク: ループがベクトル化されました。

/Qvec-report (最新のコンパイラーでは、/Qopt-report[n] /Qopt-report-phase:vec を使用) のデフォルトでは、ベクトライザーはベクトル化されたループを表示します。/Qvec-report[n] オプション (n は 0 から 5) を指定すると、ベクトライザーはベクトル化されたループとベクトル化されなかったループ、その理由などの診断情報を表示します。たとえば、/Qvec-report5 オプションを指定すると、ベクトル化されなかったループとベクトル化されなかった理由を表示します。この診断情報は、ベクトル化するループを再構成するときに役立ちます。

アドバイス/利用ガイド

異なる手法間のトレードオフ

さまざまな並列化手法は、抽象化、制御、単純性の点から分類できます。インテル® TBB と API モデルでは特定のコンパイラー・サポートは必要ありませんが、OpenMP* では必要です。OpenMP* を使用するためには、配列構文、OpenMP* ディレクティブを認識できるコンパイラーを使用する必要があります。API ベースのモデルでは、開発者は手動で並列タスクをスレッドにマップする必要があります。スレッド間には明示的な親子関係はありません。すべてのスレッドが対等です。

このようなモデルは、スレッドの作成、管理、同期におけるすべての低レベルの局面で制御能力を開発者に与えます。この柔軟性が、ライブラリー・ベースのスレッド化手法の鍵となる長所です。この柔軟性を得るためのトレードオフは、大幅なコードの変更と大量のコーディングが必要になることです。パフォーマンス・チューニングに費やした労力は、通常は異なるコア数やオペレーティング・システムのバージョンにはスケーリングしません。並列タスクは、スレッドにマップできる関数にカプセル化しなければなりません。別の短所は、ほとんどのスレッド API が難解な呼び出し規則を使用しており、1 つの引数しか受け付けないことです。その結果、関数のプロトタイプとデータ構造の変更がしばしば必要になり、プログラム設計における抽象化が損なわれます。このため、オブジェクト指向の C++ アプローチよりも C に適しています。

コンパイラー・ベースのスレッド化手法として、OpenMP* は明示的スレッド・ライブラリーに対する高レベルなインターフェイスを提供します。OpenMP* では、開発者は OpenMP* ディレクティブを使用してコンパイラーに並列化を指示します。コンパイラーが詳細な処理を行うため、明示的なスレッド化手法における複雑な作業を行う必要がなくなります。並列化に対してインクリメンタルなアプローチをとるため、アプリケーションのシリアル構造は完全な状態が維持され、大幅なソースコードの変更は必要ありません。OpenMP* 非対応コンパイラーは、単純に OpenMP* ディレクティブを無視して、シリアルコードをそのまま残します。

ただし、OpenMP* を使用すると、スレッドを細かく調整する制御力は損なわれます。また、OpenMP* では、スレッドの優先度の設定やイベントベースまたはプロセス間の同期を実行する方法が開発者に提供されません。OpenMP* は、スレッド間の明示的なマスター/ワーカーの関係に基づく fork-join スレッド化モデルです。このため、OpenMP* が適合する範囲は限定されます。

一般に、OpenMP* はデータ並列化の表現に最適で、明示的なスレッド API 手法は機能分割に最適です。OpenMP* がループ構造や C コードをサポートしていることはよく知られていますが、C++ に対しては特定のサポートがありません。OpenMP* バージョン 3.0 では、while ループや再帰構造などの不規則な構造に対するサポートの追加により OpenMP* を拡張するタスク処理がサポートされています。しかし、通常、OpenMP* は、C++ を最小限にサポートする単純な C および Fortran プログラミングを連想させることに変わりはありません。

インテル® TBB は、STL のような標準 C++ コードを使用して、汎用のスケーラブルな並列プログラミングをサポートしています。特定のコンパイラーは不要です。抽象的でさらに汎用的なオブジェクト指向アプローチに適合する、柔軟で高レベルな並列化アプローチが必要な場合は、インテル® TBB が最適な選択肢です。

インテル® TBB は共通の並列反復パターンにテンプレートを使用し、入れ子の並列化によりスケーラブルなデータ並列プログラミングをサポートします。API アプローチとは異なり、スレッドではなくタスクを指定し、インテル® TBB ランタイムを使用して、効率的な方法でライブラリーによりタスクをスレッドにマップします。インテル® TBB スケジューラーは、スケジューリングについて単一の自動的な分割統治法を優先します。スケジューラーは、ロードされたコアから休止状態のコアにタスクを移動するタスクスチールを実装しています。OpenMP* と比較すると、インテル® TBB に実装されている汎用アプローチでは、ビルトイン型に限定されない、開発者が定義した並列構造で作業することが可能です。

次の表は、インテル® Parallel Composer で利用可能なスレッド化手法を比較したものです。

手法 説明 長所 注意
明示的スレッド API 低レベル (低水準) のマルチスレッド・プログラミング用の Win32 スレッド API や Pthreads などの低レベル API
  • 最大限の制御と柔軟性
  • 特別なコンパイラー・サポートは不要
  • コードの記述、デバッグ、保守が複雑で、非常に時間がかかる
  • スレッド管理と同期はすべて開発者が行う
OpenMP*

(/Qopenmp コンパイラー・オプションを指定)

API とコンパイラー・ディレクティブを使用して C/C++ および Fortran での共有メモリーの並列プログラミングをサポートする、OpenMP.org (英語) により定義されている仕様
  • 比較的少ない労力で大幅にパフォーマンスが向上する可能性
  • ラピッド・プロトタイピングに最適
  • C/C++ および Fortran で使用可能
  • コンパイラー・ディレクティブを使用したインクリメンタルな並列化が可能
  • 開発者が並列化するコードを制御
  • 複数のプラットフォーム用のシングルソース・ソリューション
  • シリアルバージョンと並列バージョンの両方で同じコードベース
スレッドの優先順位の設定、イベントベースの実行、プロセス間の同期などのスレッドに対する制御を開発者があまり行えない
インテル® スレッディング・ビルディング・ブロック (インテル® TBB) スレッド処理の実装にかかる時間を軽減する並列アルゴリズムとコンカレント・データ構造の提供により、パフォーマンス目的のスレッド化作業を単純化するインテルの C++ ランタイム・ライブラリー
  • 特別なコンパイラー・サポートは不要
  • STL のように標準 C++ コードを使用
  • 自動のスレッド作成、管理、スケジューリング
  • スレッドではなくタスクの点からさまざまな並列化手法が可能
  • C++ プログラムに最適
  • Fortran は未サポート
自動並列化 

(/Qparallel コンパイラー・オプションを指定)

プログラム中のループ伝播の依存がないループを自動的に並列化するインテル® C++ コンパイラーの機能
  • 並列化が可能なループのマルチスレッド・コードをコンパイラーが自動的に生成
  • 他のスレッド化手法と組み合わせて使用可能
コンパイラーが静的に処理できるループはデータ依存性とエイリアス解析により並列化可能
自動ベクトル化 

(/arch: および /Qx オプションを指定)

シーケンシャルな命令を一度に複数のデータ要素で動作可能な SIMD 命令に変換することにより、インテル® プロセッサーのベクトルレベルの並列化を利用してループのパフォーマンスを最適化する手法
  • ベクトルレベルの並列化はコンパイラーが自動的に行う
  • 他のスレッド化手法と組み合わせて使用可能
プロセッサー固有のオプションが使用されている場合、生成されたコードはすべてのプロセッサーで動作しない可能性がある

「Solve the N-Queens problem in parallel」では、このドキュメントで説明されている各並列化手法を、N クイーン問題 (エイト・クイーン・パズルのより一般的なバージョン) に適用して、並列ソリューションを実装するハンズオン・トレーニングを提供しています。その他のサンプルは、インテル® C++ コンパイラーのインストール・フォルダー「Samples」サブフォルダーにあります。

関連情報

タイトルとURLをコピーしました