13    OpenMP 並列処理

Compaq C コンパイラは,OpenMP C APIに準拠した共用メモリ並列処理アプリケーションの開発をサポートします。 この API は,コンパイラ指示文,ライブラリ関数,環境変数の集合を定義し,コンパイラ,リンカ,実行時環境に指示を与えて,アプリケーションの複数の部分を並行して実行できるようにします。

OpenMP 指示文を使用すると,複数のプロセッサで並行して実行できるコードを,通常の直列的な ANSI C のソース・コードの構造を変更することなく作成できます。 これらの指示文を正しく使用すれば,マルチプロセッサ・マシンの別々のプロセッサでそのコードが同時に実行できるため,ユーザ・コードの経過時間に関する性能を大幅に向上できます。 同じソース・コードをコンパイルするときに,並列処理の指示文を無視するようにすれば,OpenMP でのコンパイルと同じ機能を実行する直列的な C 言語プログラムになります。

OpenMP C and C++ Application Programming Interface』仕様書は,インターネット上の http://www.openmp.org/specs/ で参照できます。

この章では,次の項目について説明します。

13.1    コンパイル・オプション

次の cc コマンド行オプションは,並列処理をサポートします。

-mp

コンパイラが OpenMP の手動分解プラグマと従来の手動分解指示文の両方を認識するようにします。 libots3 がリンクに含まれるようにします。 従来の手動分解指示文の詳細については,付録 D を参照してください。

-omp

コンパイラが OpenMP の手動分解プラグマだけを認識し,従来の手動分解指示文を無視するようにします。 従来の手動分解指示文の処理を除いて,-mp-omp スイッチは同じです。 つまり,-mp は従来の指示文を認識し,-omp は認識しません。

-granularity  size

異なるスレッドから安全にアクセスできるメモリ内の共用データのサイズを制御します。 size の有効な値は,bytelongword,および quadword です。

byte

1 バイト以上のすべてのデータに,メモリ内のデータを共用する別々のスレッドからアクセスできるようにします。 このオプションを使用すると,実行時の性能が低下します。

longword

自然に位置合わせされている 4 バイト以上のデータに,メモリ内のデータへのアクセスを共用する別々のスレッドから安全にアクセスできるようにします。 3 バイト以下のデータ項目および位置合わせされていないデータにアクセスすると,複数のスレッドから書き込まれたデータ項目の更新に一貫性が保たれない場合があります。

quadword

自然に位置合わせされている 8 バイトのデータに,メモリ内のデータを共用する別々のスレッドから安全にアクセスできるようにします。 7 バイト以下のデータ項目および位置合わせされていないデータにアクセスすると,複数のスレッドから書き込まれたデータ項目の更新に一貫性が保たれない場合があります。 これは,省略時の値です。

-check_omp

特定の OpenMP 構造の実行時検査が行われるようにします。 これには,無効なネストおよびその他の無効な OpenMP の事例の実行時検出が含まれます。 実行時に無効なネストが検出された場合にこのスイッチが設定されていると,実行可能プログラムは Trace/BPT トラップで異常終了します。 このスイッチが設定されていないときに無効なネストが検出されると,その場合の動作は予測できません。 たとえば,実行可能プログラムが停止することがあります。

コンパイラでは,次の無効なネスト状態を検出します。

省略時の設定では,実行時検査は行われません。

13.2    環境変数

コンパイラおよび実行時システムは,OpenMP 仕様に概要が示されている環境変数に加えて,次の環境変数を認識します。

MP_THREAD_COUNT

実行時システムが作成するスレッドの数を指定します。 省略時の設定は,プロセスで使用できるプロセッサの数です。 OMP_NUM_THREADS 環境変数は,この変数に優先します。

MP_STACK_SIZE

各スレッドについて,実行時システムが割り当てるスタック空間のバイト数を指定します。 0 を指定した場合,実行時システムでは非常に小さい,省略時の設定を使用します。 したがって,プログラムで大きな配列を PRIVATE として宣言したときは,これらを割り当てられるだけの値を指定してください。 この環境変数を使用しない場合,実行時システムは 5 MB を割り当てます。

MP_SPIN_COUNT

条件が真になるのを待つ間に,実行時システムが何回スピンするかを指定します。 省略時の設定は 16,000,000 で,これは CPU 時間の約 1 秒に相当します。

MP_YIELD_COUNT

スレッド条件変数を待ってスリープに入るまでに,実行時システムが sched_yield の呼び出しと条件の検査を何回交互に実行できるかを指定します。 省略時の設定は 10 です。

13.3    実行時性能のチューニング

OpenMP 仕様では,parallel for 構造で,使用できるスレッドに処理を分散する各種の方法を用意しています。 以降の各項では,これらの方法について説明します。

13.3.1    スケジュール・タイプとチャンクサイズの設定

スケジュール・タイプとチャンクサイズの設定の選択は,並列化されたアプリケーションの最終的な性能に良くも悪くも影響することがあります。 スケジュール・タイプとチャンクサイズに不適切な設定を選択すると,並列化されたアプリケーションの性能が,直列化の場合と同じか,またはそれ以下に低下する可能性があります。

一般的な指針を次に示します。

スケジュール・タイプとチャンクサイズの設定は,アプリケーションの性能に影響する数多くの要因のうちの 2 つに過ぎません。 性能に影響を及ぼす可能性のあるその他の要因には,次のものがあります。

13.3.2    その他の制御

あるスレッドが,他のスレッドによって発生するイベントを待つ必要がある場合は,次の 3 段階のプロセスが開始されます。

  1. そのスレッドは,特定の反復数だけスピンしながら,イベントの発生を待ちます。

  2. プロセッサを何回も他のスレッドに譲りながら,イベントが発生していないかどうかを検査します。

  3. 起こすように要求を送ってからスリープに入ります。

他のスレッドによりイベントが発生すると,スリープしているスレッドが起こされます。

環境変数 MP_SPIN_COUNTMP_YIELD_COUNT,または mpc_destroy ルーチンを使用してスレッド環境をチューニングすると,性能が向上することがあります。

13.4    プログラミング上の一般的な問題

以降の各項では,並列化されたプログラムでよく発生するエラーについて説明します。

13.4.1    範囲指定

OpenMP の parallel 構造は,そのすぐ後の構造化ブロックに適用されます。 複数の文を並列に実行する場合は,構造化ブロックを必ず中カッコ内に入れます。 次に例を示します。

#pragma omp parallel
{
   pstatement one
   pstatement two
}

この構造化ブロックは,次に示す,OpenMP の parallel 構造が最初の文にのみ適用される構造化ブロックとは全く異なります。

#pragma omp parallel
   pstatement one
   pstatement two

中カッコを使用して後続のブロックの範囲を明示的に定義することを強くお勧めします。

13.4.2    デッドロック

どのマルチスレッド・アプリケーションにも言えることですが,プログラマは実行時のデッドロック状態を回避するよう注意する必要があります。 多くの OpenMP 構造には最後に暗黙のバリアがあるため,すべてのスレッドがその構造にアクティブに加わらない場合,アプリケーションでデッドロックが生じます。 このような状態は,アプリケーションの動的なエクステントを並列化する場合により多く生じます。 次に例を示します。

worker ()
 {
 #pragma omp barrier
 }
 
 main ()
 {
 #pragma omp parallel sections
   {
   #pragma omp section
     worker();
   }
 }

この例では,すべてのスレッドが worker ルーチンに行くわけではないのに,バリアがすべてのスレッドを待つため,(複数のスレッドがアクティブなままで) デッドロックが生じます。 このような状態の検出には,-check_omp オプション (13.1 節を参照) が役に立ちます。

有効および無効な指示文のネストの詳細については,『OpenMP C and C++ Application Programming Interface』仕様書を参照してください。

13.4.3    threadprivate ストレージ

threadprivate 指示文は,ファイルの有効範囲を持ちながらも各スレッドにプライベートな変数を識別します。 スレッド数が一定である場合,これらの変数の値は維持されます。 プログラム内でスレッド数を明示的に増減したときの threadprivate 変数の値への影響は定義されていません。

13.4.4    ロックの使用

ロック制御ルーチン (『OpenMP C and C++ Application Programming Interface』仕様書を参照) を使用するには,特定の順序で呼び出す必要があります。

  1. まず,ロック変数と関連付けるロックを初期化します。

  2. 関連付けられたロックが実行スレッドで使用できるようにします。

  3. 実行スレッドのロックの所有を解除します。

  4. 終了したら,ロックとロック変数の関連付けを解除します。

これ以外の順序でロックを使用しようとすると,デッドロック状態などの予期しない動作が生じることがあります。

13.5    インプリメンテーション固有の動作

OpenMP 仕様では,いくつかの機能と省略時の値をインプリメンテーション固有のものとしています。 この節では,このような事例と Compaq C で選択されているインプリメンテーションを示します。

ネストされた並列領域に対するサポート

ネストされた並列領域があると,1 つのスレッドで構成されるチームがその領域の実行のために作成されます。

OMP_SCHEDULE の省略時の値

省略時の値は dynamic,1 です。 この値は,アプリケーションが実行時スケジュールを使用するときに,OMP_SCHEDULE が定義されていない場合に使用されます。

OMP_NUM_THREADS の省略時の値

省略時の値は,マシンのプロセッサの数と同じです。

OMP_DYNAMIC の省略時の値

省略時の値は 0 です。 このインプリメンテーションでは,スレッド・カウントの動的な調整はサポートされません。 omp_set_dynamic を 0 以外の値で使用しようとしても,実行時環境には何も作用しません。

省略時のスケジュール

for または parallel for ループにスケジュール句が含まれていない場合,スケジュール・タイプには DYNAMIC が使用され,チャンクサイズは 1 に設定されます。

flush 指示文

flush 指示文は,指示文に 1 つまたは複数の変数が指定されていても,すべての変数をフラッシュします。

13.6    デバッグ

以下の節には,OpenMP アプリケーション・プログラミング・インタフェース (API) を使用するアプリケーションの動作を診断し,デバッグする方法についてのヒントを示します。

13.6.1    デバッグに必要な背景知識

-mp または -omp オプションを使用すると,コンパイラは OpenMP 指示文を認識し,コードの指定された部分を並列処理リージョンに変換します。 コンパイラは,リージョン内のコードを取り出し,それをコンパイラが作成した別のルーチンに入れることで,並列処理リージョンを実現します。 この処理は,ルーチンが呼び出された所でソース・コードをルーチンに入れるインライン化の逆なので,アウトライン化と呼ばれます。

注意

デバッガと他のアプリケーション分析ツールを効率良く使用するためには,並列処理リージョンをアウトライン化する方法を理解しなければなりません。

並列処理リージョンの場所に,コンパイラは実行時ライブラリ・ルーチンへの呼び出しを挿入します。 実行時ライブラリ・ルーチンは,チームの中にスレーブ・スレッドを作成し (まだ作成されていない場合),チーム内のすべてのスレッドを開始し,アウトライン・ルーチンを呼び出します。 アウトライン・ルーチンからスレッドが戻ると,スレッドは実行時ライブラリに戻ります。 ライブラリはすべてのスレッドが完了するまで待ち,その後マスタ・スレッドが呼び出し元のルーチンに戻ります。 マスタ・スレッドが非並列実行を継続している間,スレーブ・スレッドは,新しい並列処理リージョンに遭遇するか,または環境変数によって決まる待ち時間 (MP_SPIN_COUNT) が経過するまで待機 (つまりスピン) します。 待ち時間が経過すると,スレーブ・スレッドは次に並列処理リージョンに遭遇するまでスリープ状態になります。

次のソース・コードに含まれる並列処理リージョンでは,変数 id は各スレッドにプライベートです。 並列処理リージョンの前にあるコードは,並列処理リージョンで使用するスレッドの数を明示的に 2 としています。 次に,並列処理リージョンは,実行しているスレッドのスレッド番号を取得し,それを printf 文で表示します。

 1
 2  main()
 3  {
 4    int id;
 5    omp_set_num_threads(2);
 6  # pragma omp parallel private (id)
 7    {
 8      id= omp_get_thread_num();
 9      printf ("Hello World from OpenMP Thread %d\n", id);
10    }
11  }

dis コマンドを用いて,上記のソース・コードから生成されたオブジェクト・モジュールを逆アセンブルすると,次のような結果が出力されます。

       _ _main_6:   [1]
0x0:    27bb0001        ldah    gp, 1(t12)
0x4:    2ffe0000        ldq_u   zero, 0(sp)
0x8:    23bd8110        lda     gp, -32496(gp)
0xc:    2ffe0000        ldq_u   zero, 0(sp)
0x10:   23defff0        lda     sp, -16(sp)
0x14:   b75e0000        stq     ra, 0(sp)
0x18:   a2310020        ldl     a1, 32(a1)
0x1c:   f620000e        bne     a1, 0x58
0x20:   a77d8038        ldq     t12, -32712(gp)
0x24:   6b5b4000        jsr     ra, (t12), omp_get_thread_num
0x28:   27ba0001        ldah    gp, 1(ra)
0x2c:   47e00411        bis     zero, v0, a1
0x30:   23bd80e8        lda     gp, -32536(gp)
0x34:   a77d8028        ldq     t12, -32728(gp)
0x38:   a61d8030        ldq     a0, -32720(gp)
0x3c:   6b5b4000        jsr     ra, (t12), printf
0x40:   27ba0001        ldah    gp, 1(ra)
0x44:   23bd80d0        lda     gp, -32560(gp)
0x48:   a75e0000        ldq     ra, 0(sp)
0x4c:   63ff0000        trapb   0x50:   23de0010        lda     sp, 16(sp)
0x54:   6bfa8001        ret     zero, (ra), 1
0x58:   221ffff4        lda     a0, -12(zero)
0x5c:   000000aa        call_pal        gentrap
0x60:   c3ffffef        br      zero, 0x20
0x64:   2ffe0000        ldq_u   zero, 0(sp)
0x68:   2ffe0000        ldq_u   zero, 0(sp)
0x6c:   2ffe0000        ldq_u   zero, 0(sp)
         main:
0x70:   27bb0001        ldah    gp, 1(t12)
0x74:   2ffe0000        ldq_u   zero, 0(sp)
0x78:   23bd80a0        lda     gp, -32608(gp)
0x7c:   2ffe0000        ldq_u   zero, 0(sp)
0x80:   a77d8020        ldq     t12, -32736(gp)
0x84:   23defff0        lda     sp, -16(sp)
0x88:   b75e0000        stq     ra, 0(sp)
0x8c:   47e05410        bis     zero, 0x2, a0
0x90:   6b5b4000        jsr     ra, (t12), omp_set_num_threads
0x94:   27ba0001        ldah    gp, 1(ra)
0x98:   47fe0411        bis     zero, sp, a1
0x9c:   2ffe0000        ldq_u   zero, 0(sp)
0xa0:   23bd807c        lda     gp, -32644(gp)
0xa4:   47ff0412        bis     zero, zero, a2
0xa8:   a77d8010        ldq     t12, -32752(gp)
0xac:   a61d8018        ldq     a0, -32744(gp)
0xb0:   6b5b4000        jsr     ra, (t12), _OtsEnterParallelOpenMP  [2]
0xb4:   27ba0001        ldah    gp, 1(ra) :     a75e0000        ldq     ra,
0(sp)
0xbc:   2ffe0000        ldq_u   zero, 0(sp)
0xc0:   23bd805c        lda     gp, -32676(gp)
0xc4:   47ff0400        bis     zero, zero, v0
0xc8:   23de0010        lda     sp, 16(sp)
0xcc:   6bfa8001        ret     zero, (ra), 1

  1. _ _main_6 は,リストの 6 行目の,ルーチン main で始まる並列処理リージョンに対して,コンパイラが作成したアウトライン・ルーチンです。 コンパイラが生成するアウトライン・ルーチンの名前は,次のような形式になっています。

    _ _original-routine-name_listing-line-number [例に戻る]

  2. _OtsEnterParallelOpenMP への呼び出しがコンパイラによって挿入され,スレッド作成と並列処理リージョンの実行の整合がとられます。 実行の制御は,すべてのスレッドが並列処理リージョンを終了するまで,_OtsEnterParallelOpenMP の内部に留まります。 [例に戻る]

13.6.2    デバッグおよびアプリケーション分析のツール

OpenMP アプリケーションをデバッグするための主要なツールは Ladebug デバッガです。 その他のツールには,Visual Threads,Atom ツールの pixiethird,および OpenMP ツール ompc があります。

13.6.2.1    Ladebug

この項では,OpenMP アプリケーションで Ladebug デバッガを使用する方法について説明します。 ここでは,従来のマルチスレッド・アプリケーションと比較して,OpenMP アプリケーションに特有の事項について説明します。 13.6.1 項 でのプログラム例を使用して,OpenMP アプリケーションのデバッグのコンセプトを示します。 マルチスレッド・プログラムのデバッグについての詳細は,『Ladebug Debugger Manual』 を参照してください。

OpenMP アプリケーションはマルチスレッドなので,通常のマルチスレッド・プログラムの場合と同じ方法でデバッグできます。 ただし,考慮すべきことがいくつかあります。

並列処理リージョンをデバッグする場合,アウトライン・ルーチン名にブレークポイントを設定します。 次の例は,Ladebug セッションの開始,並列処理リージョンへのブレークポイントの設定,実行の継続を示しています。 ユーザ・コマンドは脚注で説明しています。

> ladebug example                         [1]
Welcome to the Ladebug Debugger Version 4.0-48
------------------
object file name: example
Reading symbolic information ...done
(ladebug) stop in _ _main_6                [2]
[#1: stop in void _ _main_6(int, int) ]
(ladebug) run                             [3]
[1] stopped at [void _ _main_6(int, int):6 0x1200014e0]
      6 # pragma omp parallel private (id)
(ladebug) thread                          [4]
 Thread Name            State        Substate    Policy       Pri
 ------ --------------- ------------ ----------- ------------ ---
>*> 1 default thread    running                  SCHED_OTHER  19
(ladebug) cont                            [5]
Hello World from OpenMP Thread 0
[1] stopped at [void _ _main_6(int, int):6 0x1200014e0]
      6 # pragma omp parallel private (id)
(ladebug) thread                          [6]
 Thread Name            State        Substate    Policy       Pri
 ------ --------------- ------------ ----------- ------------ ---
>*    2 <anonymous>     running                  SCHED_OTHER  19
(ladebug) cont                            [7]
Hello World from OpenMP Thread 1
Process has exited with status 0

  1. 例のアプリケーションを指定して Ladebug セッションを開始します。 [例に戻る]

  2. アウトライン・ルーチン _ _main_6 の開始点で止めるためにブレークポイントを設定します。 [例に戻る]

  3. プログラムを起動します。 _ _main_6 の先頭で制御が止まります。 [例に戻る]

  4. 並列処理リージョンをアクティブに実行しているスレッドを表示します (この例では pthread 1,OpenMP スレッド 0)。 [例に戻る]

  5. この地点から実行を継続し,再びブレークポイントで止まる前に,OpenMP スレッド 0 の並列処理リージョンが終了して,適切な OpenMP スレッド番号で「Hello World」というメッセージが出力されることを確認します。 [例に戻る]

  6. 並列処理リージョンをアクティブに実行している次のスレッドを表示します (pthread 2,OpenMP スレッド 1)。 [例に戻る]

  7. この地点から実行を継続すると,次のメッセージが出力され,プログラムの実行が終了します。 [例に戻る]

次の例では,pthread 2 (OpenMP スレッド 1) が並列処理リージョンの実行を開始したときに,アウトライン・ルーチンの先頭にブレークポイントを設定する方法を示します。

> ladebug example
Welcome to the Ladebug Debugger Version 4.0-48
------------------
object file name: example
Reading symbolic information ...done
(ladebug) stop thread 2 in _ _main_6                            [1]
[#1: stop thread (2) in void _ _main_6(int, int) ]
(ladebug) r
Hello World from OpenMP Thread 0
[1] stopped at [void _ _main_6(int, int):6 0x1200014e0]
      6 # pragma omp parallel private (id)
(ladebug) thread
 Thread Name             State        Substate    Policy       Pri
 ------ ---------------- ------------ ----------- ------------ ---
>*    2 <anonymous>      running                  SCHED_OTHER  19
(ladebug) c
Hello World from OpenMP Thread 1
Process has exited with status 0

  1. 並列処理リージョンの開始点で OpenMP スレッド 1 (pthread 2) を止めます。 [例に戻る]

OpenMP 組込みのワーク・シェアリング構造 (for および sections 指示文) のデバッグは,前の例と同じように行います。

注意

Ladebug デバッガは,OpenMP デバッグをまだ完全にはサポートしていません。 threadprivate として宣言された変数は,Ladebug では認識されず,表示できません。

13.6.2.2    Visual Threads

OpenMP が組み込まれたプログラムは,Compaq Visual Threads (dxthreads) 製品でモニタリングできます。 この製品は,「Associated Products Volume 1」CD-ROM にあります。 詳細は,Visual Threads のオンライン・ヘルプを参照してください。

13.6.2.3    Atom および OpenMP ツール

OpenMP アプリケーションは,OpenMP アプリケーションをモニタリングするために作成された特殊なツールの ompc,Atom ベースのツール pixie (実行のプロファイル用),Third Degree (メモリ・アクセスとリークをモニタリングする third) を用いて計測できます。

ompc ツールは,関連する環境変数設定をキャプチャし,OpenMP に関する実行時ライブラリ・ルーチンの呼び出しをカウントします。 開発者の意図しない状況では,警告およびエラー・メッセージを生成して注意を促します。 最後に,環境変数の設定に基づいて,これらの実行時ライブラリ・ルーチンへの呼び出しをすべてトレースし,ルーチンが呼び出された順番を (OpenMP スレッド番号で) 通知します。 詳細は ompc(5) を参照してください。

Atom ベースの pixie ツールは,アプリケーションでの効率の良くないスレッド使用を検出するために使用します。 13.6.1 項 で説明したように,スレーブ・スレッドは,新しい並列処理リージョンの実行が始まるか,または MP_SPIN_COUNT が経過するまで待機 (つまりスピン) します。 アプリケーションの並列処理リージョン間の時間が長い場合,スレッドはスリープ状態になるまでスピンします。 pixie でアプリケーションを計測すると,アプリケーションがどこで多くの時間を費やしているかを確認できます。 アプリケーションが slave_main で CPU 時間を大量に費やしている場合は,スレッドがスピンに費やす時間が長すぎることを示しています。 このようなアプリケーションでは,MP_SPIN_COUNT の値 (省略時の値は 16000000) を小さくすると,全体の性能が良くなります。 pixie についての詳細は,第 8 章pixie(1) を参照してください。

Third Degree ツールについては,第 7 章third(1) を参照してください。

13.6.2.4    その他のデバッグ支援機能

その他のデバッグ支援機能には,次のようなものがあります。

omp_set_num_threads または mpc_destroy を呼び出すと,プログラム内でアクティブなスレッドの数を変更できます。 いずれの場合も,threadprivate で宣言されたデータや,スレーブ・スレッドに関係するデータは,アプリケーションの起動時の値で再度初期化されます。 たとえば,アクティブなスレッドの数が 4 のときに,omp_set_num_threads を呼び出してこの数を 2 に設定すると,OpenMP スレッドの 1,2,3 に対応する threadprivate データがリセットされます。 マスタ・スレッド (OpenMP スレッド 0) に対応するthreadprivate データは変更されません。

mpc_destroy についての詳細は,13.3.2 項 を参照してください。