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

同カテゴリーの次の記事

マルチスレッド・アプリケーション開発のためのガイド

コードの並列化にはさまざまな手法があります。この記事では、インテル® Parallel Composer で利用可能な手法の概要を説明し、各手法の主な長所を比較します。インテル® Parallel Composer は Windows* 上の C/C++ を使用した開発のみを対象としていますが、これらの手法の多くは Fortran や Linux* 上での開発でも有効です (特定のコンパイラーが必要な場合があります)。

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

はじめに

インテル® コンパイラーには並列実行できるコード構造を自動的に検出して最適化するいくつかの方法 (自動ベクトル化や自動並列化など) が用意されていますが、多くの場合コードの変更が必要です。挿入するプラグマや関数は、並列スレッドの分割やスケジュール実行を実際に行うランタイム・ライブラリー (インテル® Cilk™ Plus、インテル® Array Building Blocks (インテル® ArBB)、インテル® スレッディング・ビルディング・ブロック (インテル® 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.pngCilk-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; iicl /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): リマーク: 関数がベクトル化されました。

    インテル® Cilk™ Plus と要素関数についての詳細は、『インテル® C++ コンパイラー 12 ユーザー・リファレンス・ガイド』および「Elemental functions: Writing data parallel code in C/C++ using Intel® Cilk Plus」 (英語) を参照してください。

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

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

    例:

    #include "tbb/task_scheduler_init.h"
    #include "tbb/blocked_range.h"
    #include "tbb/parallel_for.h"
    #include 
    void foo() {
      tbb::task_scheduler_init init;
      size_t length = 1000000;
      std::vector a(length, 2), b(length, 3), c(length, 0);
      tbb::parallel_for(tbb::blocked_range(0, length),
      [&](const tbb::blocked_range &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 コンパイラー・オプションを指定してコードをコンパイルしてください。

    インテル® 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 の仕様をサポートしています。詳細は、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 パラレライザーの診断メッセージのレベルを制御します。このオプションを使用するには、/Qopenmp オプションを指定する必要があります。n を指定しない場合、デフォルトの /Qopenmp-report1 が使用され、正常に並列化されたループ、領域、セクションを示す診断メッセージが表示されます。

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

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

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

    例:

    #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 (¶ms[i]), 0, NULL);
      }
      // すべてのスレッドが完了するまで待機してスレッドを結合します
      WaitForMultipleObjects (num_thr, threads, true, INFINITE);
      //  メモリーを解放します
      delete[] params;
      delete[] threads;
    }

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

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

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

    インテル® 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 のデフォルトでは、自動パラレライザーは自動並列化されたループを表示します。/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 のデフォルトでは、ベクトライザーはベクトル化されたループを表示します。/Qvec-report[n] オプション (n は 0 から 5) を指定すると、ベクトライザーはベクトル化されたループとベクトル化されなかったループ、その理由などの診断情報を表示します。たとえば、/Qvec-report5 オプションを指定すると、ベクトル化されなかったループとベクトル化されなかった理由を表示します。この診断情報は、ベクトル化するループを再構成するときに役立ちます。

    アドバイス/利用ガイド

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

    さまざまな並列化手法は、抽象化、制御、単純性の点から分類できます。インテル® TBB と API モデルでは特定のコンパイラー・サポートは必要ありませんが、インテル® Cilk™ Plus と OpenMP では必要です。インテル® Cilk™ Plus、OpenMP を使用するためには、インテル® Cilk™ Plus キーワード、配列構文、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 プログラミングを連想させることに変わりはありません。

    インテル® ArBB は、汎用ベクトル並列プログラミングのソリューションを提供します。開発者は、低レベルの並列化メカニズムやハードウェア・アーキテクチャーの依存性を考慮する必要がありません。インテル® ArBB は、すべての標準的なコンパイラーおよび IDE との互換性のために C++ 言語拡張を使用し、特定のコンパイラーに関連付けられていません。以下のような場合に、インテル® ArBB を使用してください。

    • データ並列形式でアルゴリズムを表現するのが自然な場合
      • 配列に対する操作
      • 配列のすべての要素に並列に適用される要素関数
      • コンパイラーにコア、スレッド、および SIMD 実行リソースの最適な使用の判断を委ねる場合
      • JIT コンパイルに基づき、「一度のコンパイルでさまざまな環境で実行する」デプロイメント・モデルに興味がある場合
    • 決定性のある実行に興味があり、インテル® ArBB のメモリー空間の消費のランタイム管理を利用している場合

    インテル® Cilk™ Plus は、データ並列とタスク並列の両方を C/C++ 言語でサポートしています。単純な fork-join 並列化を提供する 3 つの簡単なキーワードにより、既存のシリアルプログラムを並列化する最も容易な方法です。また、ここで説明する並列モデルで、並列スレッドの呼び出しにかかるオーバーヘッドが最も低いものがインテル® Cilk™ Plus です。cilk キーワードは、Cilk に対応していないコンパイラーでは無視され、プリプロセッサーにより同等のシリアルバージョンに置換されます (そのため、ヘッダーファイルが提供されます)。ただし、配列表記は簡単には無視できません。

    それ自身 (例えば入れ子された OpenMP) や他のスレッド化モデルと組み合わせることが難しい OpenMP とは違い、インテル® Cilk™ Plus はスレッドのオーバーサブスクリプションを引き起こすことなく、インテル® TBB やインテル® ArBB とうまく組み合わせることができます。開発者は、コードの大部分にはインテル® Cilk™ Plus を使用し、「スコープロック」や「並列ハッシュ」のようなほかの並列化構造と並列データ構造が必要な箇所にはインテル® TBB を使用して並列処理を実装できます。

    インテル® 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 で使用可能
    • コンパイラー指示句を使用したインクリメンタルな並列化が可能
    • 開発者が並列化するコードを制御
    • 複数のプラットフォーム用のシングルソース・ソリューション
    • シリアルバージョンと並列バージョンの両方で同じコードベース
    スレッドの優先順位の設定、イベントベースの実行、プロセス間の同期などのスレッドに対する制御を開発者があまり行えない
    インテル® Cilk™ Plus C/C++ (cilk spawn、cilk sync、cilk for) 向けの新しいキーワード、競合状態を回避するレデューサー、ベクトル化を活用するための配列表記
    • 元のプログラムのシリアル構成とセマンティクスを保持する構文
    • 1 つのパッケージでマルチコアと SIMD に対応
    • 構成可能で、理由付けが簡単
    • コンパイラー・サポートが必要
    • Fortran は未サポート
    • スレッド化の細かい調整はできない
    インテル® ArBB 計算負荷の高いデータ並列アプリケーション向けのインテルの標準 C++ ライブラリー・インターフェイスとランタイム。開発者は、低レベルの並列化メカニズムやハードウェア・アーキテクチャーの依存性を考慮する必要がない。インテル® ArBB は、すべての標準コンパイラーおよび IDE と互換性のある C++ 言語拡張を使用。
    • 使いやすさ - 単純な構文により、開発者はハイレベルのアルゴリズムに専念できる
    • パフォーマンス - 構文でエイリアスを不可とし、ランタイムシステムで強力な最適化が可能
    • 安全な設計 - インテル® ArBB および C/C++ オブジェクトには個別のメモリー空間を使用。インテル® ArBB オブジェクトは、インテル® ArBB 関数でのみ処理可能。
    • シーケンシャル・セマンティクス - 開発者はスレッド、ロック、その他の下位構造を使用しないで、それらに関連する複雑性を回避
    • フォワード・スケーラビリティー - 将来のインテル® プロセッサーのコア数や SIMD 幅の増加に応じて自動でスケーリング
    • 手動によるコード・チューニングの代用ではない – 手動でチューニングしたパフォーマンスはワークロードに依存する確率が高い
    • インテル® ArBB ランタイム・コンパイラーにメモリー、スレッド、ベクトル化に対し、より多くの制御を与える。できるだけ多くの制御を利用し、明示的なスレッド化 API を使用して、独自の SIMD を記述したい開発者には不向き。
    • JIT コンパイルプロセスでは、初回実行時にオーバーヘッドが大きくなる場合がある。そのため、コードを 1 回だけ実行し、コードで「ウォームアップ」や初期化を行う余裕がない場合には不向き。
    • すべてのデータ型を ArBB 型に変換し、ArBB 関数インターフェイスを使用する必要がある。つまり、コード、入力、出力、データ型、関数/メソッドを変更し、ArBB ランタイムがそれらの文をキャプチャーして、複数のコア向けに最適化できるようにしなければならない。
    インテル® スレッディング・ビルディング・ブロック (インテル® TBB) スレッド処理の実装にかかる時間を軽減する並列アルゴリズムとコンカレント・データ構造の提供により、パフォーマンス目的のスレッド化作業を単純化するインテルの C++ ランタイム・ライブラリー
    • 特別なコンパイラー・サポートは不要
    • STL のように標準 C++ コードを使用
    • 自動のスレッド作成、管理、スケジューリング
    • スレッドではなくタスクの点からさまざまな並列化手法が可能
    • C++ プログラムに最適
    • Fortran は未サポート
    自動並列化 

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

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

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

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

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

    関連情報

    インテル® ソフトウェア・ネットワークの Parallel Programming Community (http://software.intel.com/en-us/parallel/ (英語))

    インテル® コンパイラー、ドキュメント、ホワイトペーパー、ナレッジベースに関する一般的な情報 (英語)

    『The Software Optimization Cookbook (2nd Edition) High performance Recipes for the Intel Architecture』 (英語)

    インテル® ソフトウェア・ネットワーク.・フォーラム (英語)

    完全な仕様と指示句のリストを含む OpenMP に関する情報 (英語)

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

    インテル® スレッディング・ビルディング・ブロック - オープンソース (英語)

    James Reinders, 『 Intel Threading Building Blocks: Outfitting C++ for Multi-core Processor Parallelism』 O'Reilly Media, Inc. Sebastopol, CA, 2007 (英語)

    インテル® コンパイラー最適化クイック・リファレンス・ガイド

    コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

    関連記事