Tru64 UNIX システムでは,Compaq C プログラムの並列処理は,次の 2 つの方法でサポートされます。
OpenMP インタフェース -- OpenMP Architecture Review Board により定義された並列処理インタフェース
従来の並列処理インタフェース -- OpenMP インタフェース以前に開発された並列処理インタフェース
この付録では,従来の並列処理インタフェースについて説明します。 つまり,OpenMP インタフェースの実装以前にサポートされた言語機能について説明します。 OpenMP インタフェースについての詳細は,第 13 章を参照してください。
注意
従来のインタフェースを使用しているプログラマは,業界標準である OpenMP インタフェースへの変換を考慮する必要があります。
アプリケーションを並列処理に変換する場合,または新しい並列処理アプリケーションを作成する場合は,OpenMP インタフェースを使用する必要があります。
この付録を理解するには,スレッドとは何か,データ・アクセスがスレッド・セーフであるとは何を意味するか,といったマルチプロセシングの基本的な概念を理解している必要があります。
並列処理の指示文には,ANSI C の
#pragma
前処理指示文を使用します。
これは,実装で定義された動作を C 言語に追加するための標準的な C のメカニズムです。
このため,この付録では並列処理指示文
(または並列指示文) および並列処理プラグマという用語は置き換えて使用することが可能です。
この付録には,次のトピックに関する情報が含まれます。
並列処理プラグマの使用に適用される一般的なコード記述規則 (D.1 節)
並列処理プラグマの構文 (D.2 節)
実行時にスレッド・リソースの割り当てを制御するために使用可能な環境変数 (D.3 節)
この節では,すべての並列処理プラグマに適用される一般的なコーディング規則,およびプラグマの一般的な使用方法の概要を説明します。
D.1.1 一般的なコーディング規則
多くの場合,並列処理プラグマのコーディング規則は,Compaq C で記述された他のほとんどのプラグマの規則に従います。 たとえば,マクロ置換はプラグマでは実行されません。 その他の場合,並列処理プラグマは Compaq C の他のいかなるプラグマとも異なっています。 これは,他のプラグマが一般にコンパイラ状態の設定 (メッセージまたはアラインメント) などの機能を実行するのに対し,これらのプラグマはステートメントであるためです。 次に例を示します。
プラグマは,関数本体の内部になければなりません。
プラグマは,プログラムの実行に影響を与えます (付録で説明)。
ほとんどのプラグマは,次に続く文に適用されます。 プラグマを複数の文に適用する場合,複合文を使用する必要があります (つまり,文を中カッコで囲む必要があります)。
いくつかのプラグマには,補足情報を指定する修飾子を続けて指定することができます。
これらの修飾子の使用を容易にするため,修飾子を含む行が
#pragma
で始まる場合,各修飾子は並列処理プラグマに続く行に指定することができます。
次に例を示します。
#pragma parallel if(test_function()) local(var1, var2, var2)
上の文は,次のように記述することもできます。
#pragma parallel #pragma if(test_function()) #pragma local(var1, var2, var2)
修飾子自体は複数行に分割できないことに注意してください。 たとえば,上のコードは次のように記述することはできません。
#pragma parallel #pragma if(test_function()) local(var1, #pragma var2, var2)
#pragma parallel
指示文は,並列領域の開始に使用されます。
#pragma parallel
指示文に続く文は,並列領域の範囲を限定する働きをします。
一般的にこれは,C の通常文 (他の並列処理指示文を含む場合も含まない場合もある) を含む複合文,または別の並列処理指示文 (この場合,並列領域はその 1 文で構成される) になります。
並列領域を制限する複合文の中では,他の並列処理指示文により制御されない通常の C 文は,単に各スレッド上で実行されるだけです。
他の並列処理指示文により制御される並列領域内の C 文は,指示文のセマンティクスに従って実行されます。
他のすべての並列処理プラグマは,#pragma critical
を除き,構文的に並列領域内にある必要があります。
並列領域内の最も一般的なコードは,スレッドにより実行される for ループに関するものです。
このような for ループでは,その前に
#pragma pfor
を記述する必要があります。
この構造により,異なるスレッドが for ループの異なる繰り返しを実行することが可能になり,プログラムの実行速度が速くなります。
次の例は,ループの並列実行で使用できるプラグマです。
#pragma parallel local(a) #pragma pfor iterate(a = 1 ; 1000 ; 1) for(a = 0 ; a < 1000 ; a++) { <loop code> }
並列に実行されるループは,特定の特性に従う必要があります。 次の事項が含まれます。
インデックス変数は,for 文の 3 番目の式を除き,ループによって変更されることはありません。 さらに,for 文の 3 番目の式は,インデックス変数を常に同じ量だけ修正します。
ループの繰り返しは,それぞれ独立していなければなりません。 つまり,ループのある繰り返しにより実行された計算は,他の繰り返しの結果に依存してはなりません。
ループの繰り返しの数は,ループの開始前に確定していなければなりません。
プログラマは,並列ループがこれらの制限に従うことを確認する責任があります。
また,並列処理には,いくつかの異なるコード・ブロックを並列に実行するという使用法もあります。
#pragma psection
および
#pragma sections
指示文が,この目的で使用されます。
次のコードは,これらの指示文の使用法を示します。
#pragma parallel #pragma psection { #pragma section { <code block> } #pragma section { <code block> } #pragma section { <code block> } }
コード・ブロックには,さらに特定の制限が適用されます。 たとえば,あるコード・ブロックは,他のコード・ブロックで実行された計算結果に依存してはなりません。
並列領域に指定可能なコードには,シリアル・コードもあります。
シリアル・コードは,#pragma pfor
と
#pragma psection
のいずれにも記述されません。
この場合,並列領域の実行用に作成されたすべてのスレッドが同一のコードを実行します。
これはリソースの浪費に見えますが,シリアル・コードを 2 つの
#pragma pfor
ループまたは 2 つの
#pragma psection
ブロックの間に配置するのは,多くの場合有用な方法です。
シリアル・コードはすべてのスレッドにより実行されますが,この構造は,最初の
pfor
または
psection
の後に並列領域をクローズし,2 番目の
pfor
または
psection
の前に別の並列領域をオープンするよりも効率的です。
これは,並列領域の作成およびクローズの際に実行時のオーバヘッドが発生するためです。
シリアル・コードを並列領域に配置する際には注意する必要があります。 次の文は予想できない結果となる可能性があります。
a++; b++
予想できない結果が発生するのは,すべてのスレッドがこの文を実行するために,変数
a
および
b
が何回も増分されるためです。
この問題を避けるためには,シリアル・コードを
#pragma one
processor
ブロックで囲みます。
次に例を示します。
#pragma one processor { a++; b++ }
このコードが実行されるまで,スレッドはこのコード以降を実行できないことに注意してください。
D.1.3 並列指示文のネスト
Compaq C は,ネストした並列領域を現在サポートしていません。 構文上,並列領域が別の並列領域に含まれる場合,コンパイラはエラーを発生します。 ただし,並列領域内で実行されるルーチンが別のルーチンを呼び出し,呼び出されたルーチンが並列領域に入ろうとする場合,2 番目の並列領域は直列的に実行されるため,エラーは発生しません。
#pragma parallel
を除き,ほとんどの並列構造は他の並列構造を実行することはできません。
たとえば,#pragma pfor
,#pragma one processor
,#pragma section
,または
#pragma critical code block
コードの実行中に実行可能な他の並列処理構造は
#pragma critical
だけです。
1 つの並列処理プラグマが構文的に別の並列処理プラグマ内にネストしている場合,コンパイラは不正な操作を示すエラーを発生します。
ただし,コード・ブロック内で実行中のコードがあるルーチンに制御を移し,続いてそのルーチンがこれらの指示文のいずれかを実行する場合,その動作は予測できません。
(この付録の最初に説明したように,#pragma critical
を除くすべての並列処理プラグマは,構文的に
#pragma parallel
領域内にある必要があります。
)
D.2 並列処理プラグマの構文
ここでは,各並列処理プラグマの構文について説明します。
次の並列処理プラグマは,従来の並列処理インタフェースによりサポートされます。
#pragma parallel
-- コードの並列領域を示します (D.2.1 項)。
#pragma pfor
-- 並列に実行されるループをマークします (D.2.2 項)。
#pragma psection
-- 大量のコード・セクションを開始します。
各コード・セクションは,他のコード・セクションと並列に実行されます (D.2.3 項)。
#pragma section
-- psection 領域内の各コード・セクションを指定します (D.2.3 項)。
#pragma critical
-- 一度に 1 つのスレッドだけを実行可能にするためにコードのクリティカルな領域へのアクセスを保護します (D.2.4 項)。
#pragma one processor
-- 1 つのスレッドだけで実行されるコード・セクション (D.2.5 項)。
#pragma synchronize
-- すべてのスレッドがこのポイントに達するまでスレッドを停止します (D.2.6 項)。
#pragma enter gate
および
#pragma exit gate
-- より複雑な同期形式。
すべてのスレッドが入力ゲートを通過するまで,終了ゲートを出ることは許可されません (D.2.7 項)。
#pragma parallel
指示文は,並列領域のコードをマークします。
このプラグマの構文は,次のとおりです。
#pragma parallel [parallel-modifiers...] statement-or-code_block
#pragma parallel
の
parallel-modifiers
は,次のとおりです。
local(variable-list) byvalue(variable-list) shared(variable-list) if (expression) [[no]ifinline] numthreads(numthreads-option)
local
,
byvalue
,
shared
修飾子local
,
byvalue
, および
shared
修飾子に対する引数
variable-list
は,プログラムですでに宣言された変数をコンマで区切ってリストにしたものです。
任意の数の
local
,byvalue
,および
shared
修飾子を指定できます。
多数の変数を必要とする修飾子がある場合,これは有用です。
shared
および
byvalue
修飾子に続く変数は,各スレッドにより共有されます。
local
修飾子に続く変数は,各スレッドに対する固有の変数となります。
領域外の変数値が,領域内に渡されることはありません。
領域内では,local
修飾子の変数値は未定義です。
local リストに変数を配置すると,並列領域内で変数を定義する場合と同様の効果があります。
これらの修飾子は,他の C コンパイラとの互換性を維持する目的のみで提供されています。
Compaq C では,並列領域外で宣言されたアクセス可能なすべての変数は,並列領域内でアクセスおよび変更が可能であり,またすべてのスレッドから共有されます (変数が
local
修飾子内で指定されていない限り)。
次に例を示します。
int a,b,c; #pragma parallel local(a) shared(b) byvalue(c) { <code that references a, b, and c> }
上の文は,次のように記述することもできます。
int a,b,c; #pragma parallel { int a; <code that references a, b, and c> }
if
修飾子if
修飾子に続く式には,並列領域内のコードが,多くのスレッドにより実際に並列に実行されるか,それとも単一のスレッドにより直列に実行されるを決定する条件を指定します。
条件がゼロ以外の場合,コードは並列に実行されます。
並列実行を行うかどうかの決定を実行時まで延期する場合にも,この修飾子を使用します。
少量のコードを並列に実行すると,直列に実行する場合よりも時間がかかることに注意してください。 これは,コードの並列実行に必要なスレッドの生成と削除に関係したオーバヘッドのためです。
noifinline
修飾子noifinline
修飾子は,if
修飾子が存在する場合にだけ使用できます。
省略時の値である
ifinline
は,コンパイラに対し並列領域内に 2 つのバージョンのコードを生成するよう指示します。
1 つは
if
式がゼロでない場合に並列処理を行うため,もう 1 つは
if
式がゼロの場合に直列処理を行うためです。
noifinline
修飾子は,コンパイラに対し,1 つの形式のコードだけを生成するよう指示します。
noifinline
修飾子を使用すると,生成されるコードの量は少なくなりますが,if
式の値がゼロの場合,コードの実行速度は低下します。
numthreads
修飾子numthreads-option は,次のいずれかになります。
min=
expr1,
max=
expr2
percent=
expr
どの場合にも,式が評価する値は正の整数値でなければなりません。
numthreads(
expr)
は,numthreads(min=0,max=
expr)
と等価になります。
min
が指定されると,領域の実行に
expr1
スレッド (またはそれ以上) が利用可能な場合にのみコードは並列に実行されます。
max
が指定された場合,並列領域は
expr2
スレッド以外では実行されません。
percent
が指定された場合,並列領域は実行可能なスレッドの
expr
パーセントで実行されます。
並列領域の例は,次のとおりです。
#pragma parallel local(a,b) if(func()) numthreads(x) { code }
func
がゼロ以外の値を返す場合に,このコード領域は実行されます。
並列に実行される場合,最大
x
のスレッドが使用されます。
並列領域内では,各スレッドは変数
a
と
b
のローカル・コピーを保持します。
他の変数はすべて,複数のスレッドにより共有されます。
D.2.2 #pragma pfor
#pragma pfor
指示文は,ループに並列実行用のマークを付けます。
構文上,#pragma pfor
は並列領域内にのみ記述できます。
このプラグマの構文は,次のとおりです。
#pragma pfor iterate(iterate-expressions) [pfor-options] for-statement
上に示したように,#pragma pfor
に続いて
iterate
修飾子を指定する必要があります。
iterate-expressions
の書式は,for ループと同様です。
index-variable=
expr1;
expr2;
expr3
index-variable
=
expr1
は,#pragma pfor
に続く
for
文の最初の式と一致している必要があります。
正しい動作を保証するために,index-variable
は並列領域に対してローカルに存在しなければなりません。
expr2 式には,ループが実行される回数を指定します。
expr3 式には,ループの各繰り返しでインデックス変数に追加される値を指定します。
iterate-expressions
は,#pragma pfor
に続く
for
文内の式と密接に関連しています。
iterate-expressions
内で提供される情報に,for ループの実行方法を正確に反映させるのは,プログラマの責任です。
pfor-options は次のようになります。
schedtype(schedule-type) chunksize(expr)
schedtype
オプションは,実行時スケジューラに対し,利用可能なスレッド間で繰り返しを分割する方法を指定します。
有効な
schedule-type
値は次のとおりです。
simple
-- スケジューラは,実行可能なすべてのスレッド間で繰り返しを均等に分割します。
省略時には,この値が使用されます。
dynamic
-- スケジューラは各スレッドに対し,chunksize
式で指定された繰り返し数を指定します。
interleave
-- これは,作業が交互にスレッドに割り当てられることを除き,dynamic
と同じです。
gss
-- スケジューラは,各スレッドにさまざまな繰り返し数を割り当てます。
これは
dynamic
と類似していますが,各スレッドに対して chunksize で指定された固定値を割り当てるのではなく,大きな数で始まり小さな数で終わるように繰り返し数を割り当てます。
dynamic
または
interleave
の
schedtype
には
chunksize
オプションを指定する必要があります。
このオプションは,繰り返しの回数を指定するために使用されます。
D.2.3 #pragma psection および #pragma section
#pragma psection
および
#pragma section
指示文では,相互に並行して実行されるコード・セクションを指定します。
これらの指示文は,並列領域内でのみ記述できます。
これらのプラグマの構文は,次のとおりです。
#pragma psection
{
#pragma section
stmt1
#pragma section
stmt2. . .
#pragma section
stmtn
}
これらのプラグマには,修飾子はありません。
#pragma psection
に続いて,カッコで囲まれたコード・ブロックを記述します。
コード・ブロックは,#pragma section
指示文およびそれに続く単独の文またはカッコで囲まれた文のグループで構成されます。
psection
コード・ブロック内には,任意の数の
#pragma section
指示文を指定できます。
D.2.4 #pragma critical
#pragma critical
指示文では,同時に複数のスレッドにより実行されるコード・セクションを指定します。
このプラグマの構文は,次のとおりです。
#pragma critical [lock-option] statement-or-code-block
lock-option は,次のいずれかの値になります。
block
-- ロックは,このクリティカル・セクションに固有です。
このクリティカル・セクションの実行中に,他のスレッドは他のクリティカル・セクションを実行できますが,このクリティカル・セクションを実行できるのは 1 つのスレッドだけです。
このオプションは,並列領域内のクリティカル・セクションにのみ指定可能です。
region
-- ロックは,この並列領域に固有です。
並列領域外のコードを実行する他のスレッドは,他のクリティカル・セクションを実行できますが,並列領域内の他のクリティカル・セクションを実行することはできません。
このオプションは,並列領域内のクリティカル・セクションのみに指定可能です。
global
-- グローバル・ロックです。
このクリティカル・セクションの実行中は,他のクリティカル・セクションを実行することはできません。
省略時はこの値が使用されます。
expr -- ユーザによるロック変数を指定する式です。 この場合,式は 32 ビットまたは 64 ビットの整数変数である必要があります。
#pragma one processor
指示文は,単独のスレッドにより実行されるコード・セクションを示します。
この指示文は,並列領域内にのみ記述できます。
このプラグマの構文は,次のとおりです。
#pragma one processor statement-or-code-block
#pragma synchronize
指示文は,すべてのスレッドがこのポイントに達するまで,次の文が実行されないようにします。
この指示文は,並列領域内にのみ記述できます。
このプラグマの構文は,次のとおりです。
#pragma synchronize
D.2.7 #pragma enter gate および #pragma exit gate
#pragma enter gate
および
#pragma exit gate
指示文を使用すると,#pragma synchronize
よりも柔軟な同期が可能になります。
これらの指示文は,並列領域内にのみ記述できます。
領域内の各
#pragma enter gate
は,対になる
#pragma exit gate
が必要です。
これらのプラグマの構文は次のとおりです。
#pragma enter gate (name)
#pragma exit gate (name)
name
は,各ゲートを示す識別子です。
ゲート名は,独自の名前空間内にあります。
たとえば,ゲート名
foo
は変数名
foo
とは明確に区別されています。
ゲート名は,使用前に宣言されることはありません。
このタイプの同期は,次のように機能します。
#pragma exit gate
以降の文は,すべてのスレッドが対応する
#pragma enter gate
に達するまで,いかなるスレッドも実行することはできません。
D.3 環境変数
特定の場面での並列コード実行は,プログラム開始時のプロセス内の環境変数の値によって制御可能です。 プログラムで最初に並列実行が開始される際にチェックされる環境変数は,現在のところ次のとおりです。
MP_THREAD_COUNT
-- 作成するスレッドの数を実行時システムに示します。
省略時には,作成するスレッド数としてシステムのプロセッサ数を使用します。
MP_CHUNK_SIZE
-- 使用するチャンクサイズを示します。
ユーザが
RUNTIME
スケジュール・タイプを要求した場合,またはチャンクサイズを必要とする別のスケジュール・タイプを要求する際にチャンクサイズを省略した場合に,使用するチャンクサイズを実行時システムに示します。
MP_STACK_SIZE
-- 実行時システムに対し,スレッド作成時に各スレッドに何バイトのスタック・スペースを割り当てるかを示します。
省略時の値は非常に小さいため,大きな配列をローカルとして宣言する場合には,それらの配列を割り当てられるほど大きなスタックを指定する必要があります。
MP_SPIN_COUNT
-- 実行時システムに対し,条件が真になるのを待つ間,スピンする回数を示します。
MP_YIELD_COUNT
-- Pthread 条件変数を待ちながらスリープ状態に入る前に,sched_yield
の呼び出しと条件のテストを交互に何回実行するかを実行時システムに示します。
コマンド行シェルの規則に従って,これらの環境変数を整数値に設定することができます。 環境変数が設定されていない場合,実行時システムは適正と思われる規定値を選択します (一般的に,経過時間が最小となるようにリソースを割り当てます)。