D    並列処理 -- 従来の方法

Tru64 UNIX システムでは,Compaq C プログラムの並列処理は,次の 2 つの方法でサポートされます。

この付録では,従来の並列処理インタフェースについて説明します。 つまり,OpenMP インタフェースの実装以前にサポートされた言語機能について説明します。 OpenMP インタフェースについての詳細は,第 13 章を参照してください。

注意

従来のインタフェースを使用しているプログラマは,業界標準である OpenMP インタフェースへの変換を考慮する必要があります。

アプリケーションを並列処理に変換する場合,または新しい並列処理アプリケーションを作成する場合は,OpenMP インタフェースを使用する必要があります。

この付録を理解するには,スレッドとは何か,データ・アクセスがスレッド・セーフであるとは何を意味するか,といったマルチプロセシングの基本的な概念を理解している必要があります。

並列処理の指示文には,ANSI C の #pragma 前処理指示文を使用します。 これは,実装で定義された動作を C 言語に追加するための標準的な C のメカニズムです。 このため,この付録では並列処理指示文 (または並列指示文) および並列処理プラグマという用語は置き換えて使用することが可能です。

この付録には,次のトピックに関する情報が含まれます。

D.1    並列処理プラグマの使用

この節では,すべての並列処理プラグマに適用される一般的なコーディング規則,およびプラグマの一般的な使用方法の概要を説明します。

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)

D.1.2    一般的な使用法

#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>
}

並列に実行されるループは,特定の特性に従う必要があります。 次の事項が含まれます。

プログラマは,並列ループがこれらの制限に従うことを確認する責任があります。

また,並列処理には,いくつかの異なるコード・ブロックを並列に実行するという使用法もあります。 #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    並列処理プラグマの構文

ここでは,各並列処理プラグマの構文について説明します。

次の並列処理プラグマは,従来の並列処理インタフェースによりサポートされます。

D.2.1    #pragma parallel

#pragma  parallel 指示文は,並列領域のコードをマークします。 このプラグマの構文は,次のとおりです。

#pragma parallel [parallel-modifiers...] statement-or-code_block

#pragma  parallelparallel-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 は,プログラムですでに宣言された変数をコンマで区切ってリストにしたものです。 任意の数の localbyvalue,および 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
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 のスレッドが使用されます。 並列領域内では,各スレッドは変数 ab のローカル・コピーを保持します。 他の変数はすべて,複数のスレッドにより共有されます。

D.2.2    #pragma pfor

#pragma  pfor 指示文は,ループに並列実行用のマークを付けます。 構文上,#pragma  pfor は並列領域内にのみ記述できます。 このプラグマの構文は,次のとおりです。

#pragma pfor iterate(iterate-expressions) [pfor-optionsfor-statement

上に示したように,#pragma  pfor に続いて iterate 修飾子を指定する必要があります。 iterate-expressions の書式は,for ループと同様です。

index-variable = expr1 ; expr2 ; expr3
 

iterate-expressions は,#pragma  pfor に続く for 文内の式と密接に関連しています。 iterate-expressions 内で提供される情報に,for ループの実行方法を正確に反映させるのは,プログラマの責任です。

pfor-options は次のようになります。

schedtype(schedule-type)
chunksize(expr)
 

schedtype オプションは,実行時スケジューラに対し,利用可能なスレッド間で繰り返しを分割する方法を指定します。 有効な schedule-type 値は次のとおりです。

dynamic または interleaveschedtype には 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-optionstatement-or-code-block

lock-option は,次のいずれかの値になります。

D.2.5    #pragma one processor

#pragma  one  processor 指示文は,単独のスレッドにより実行されるコード・セクションを示します。 この指示文は,並列領域内にのみ記述できます。 このプラグマの構文は,次のとおりです。

#pragma one processor statement-or-code-block

D.2.6    #pragma synchronize

#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    環境変数

特定の場面での並列コード実行は,プログラム開始時のプロセス内の環境変数の値によって制御可能です。 プログラムで最初に並列実行が開始される際にチェックされる環境変数は,現在のところ次のとおりです。

コマンド行シェルの規則に従って,これらの環境変数を整数値に設定することができます。 環境変数が設定されていない場合,実行時システムは適正と思われる規定値を選択します (一般的に,経過時間が最小となるようにリソースを割り当てます)。