インテル® C++ コンパイラー 17.0 デベロッパー・ガイドおよびリファレンス

OpenMP* の高度な問題

ここでは、OpenMP* ライブラリー関数と環境変数の使用方法について説明し、OpenMP* を使用してパフォーマンスを向上するためのいくつかのガイドラインを示します。

OpenMP* は、特定の関数呼び出しおよび環境変数を提供します。ここで使用する主要な関数および環境変数については、次のトピックを参照してください。

この関数呼び出しを使用するには、omp.h ヘッダーファイルをインクルードします。 このファイルは、コンパイラーのインストール時に INCLUDE ディレクトリーにインストールされ、[Q]openmp オプションを使用してアプリケーションをコンパイルします。

次の例では、OpenMP* 関数を使用してアルファベットを出力する方法といくつかの重要な概念について説明します。

  1. プラグマの代わりに関数を使用するには、コードを書き換える必要があります。コードの書き換えには、追加のデバッグ、テスト、メンテナンスが伴います。
  2. OpenMP* サポートなしにコンパイルすることは困難です。
  3. 次のループではスレッド数が 26 の倍数でない場合、アルファベットの文字はすべて出力されません。このように、単純なバグは簡単に引き起こされます。
  4. ワークキューのアルゴリズムを独自に作成しない限り、ループ・スケジュールの調整ができなくなります。 ワークキューのアルゴリズムを独自に作成する場合、一般的には例に示すような STATIC スケジュールが多く、自らのスケジュールによって制限されることになります。

#include <stdio.h>
#include <omp.h>

int main(void) {
	int i;
	omp_set_num_threads(4);

	#pragma omp parallel private(i)	{
		 // OMP_NUM_THREADS が 26 の倍数ではない。
	 	// これは、このコードでは大きな問題。
 		int LettersPerThread = 26 / omp_get_num_threads();
	 	int ThisThreadNum = omp_get_thread_num();
		 int StartLetter = 'a'+ThisThreadNum*LettersPerThread;
		 int EndLetter = 'a'+ThisThreadNum*LettersPerThread+LettersPerThread;
		
		 for (i=StartLetter; i<EndLetter; i++) { printf("%c", i); }
	}
	printf("\n");
  return 0;
}

スレッド・アプリケーションのデバッグには細心の注意が必要です。これは、デバッガーによってランタイム時のパフォーマンスが左右され、競合状態が表面化しないことがあるためです。print 文でさえも、問題を発見しにくくすることがあります。これは、PRINT 文が、同期およびオペレーティング・システム関数を使用するためです。OpenMP* 自体も、プライベート変数と共有変数を区別するために追加の構造を挿入するため、さらに問題を複雑にします。OpenMP* をサポートするデバッガーを使用することにより、変数を検証しステップ実行することが可能になります。また、インテル® Inspector を使用して、発見が困難なスレッド化エラーを分析して検出することができます。高度なデバッグツールを使用しなくても、排除処理が問題の特定に役立つこともあります。

誤りの多くは競合状態です。ほとんどの競合状態は、本来ならばプライベート変数として宣言されるべき共有変数によって引き起こされます。最初に、並列領域内の変数から検証し、必要に応じて変数がプライベートとして宣言されていることを確認します。次に、並列領域内の関数呼び出しを確認します。デフォルトでは、スタックで宣言される変数はプライベートですが、C/C++ では、static キーワードによって変数はグローバルヒープに配置されるため、OpenMP* ループで共有されます。

次に示す default(none) 節は、見つけるのが困難な変数を探すのに役立ちます。 default(none) を指定する場合、各変数はデータ共有属性節とともに宣言する必要があります。

#pragma omp parallel for default(none) private(x,y) shared(a,b)

その他のよくある誤りは、初期化されていない変数の使用です。プライベート変数は、並列構造の入口では初期値を持っていません。firstprivate 節および lastprivate 節を使用して初期化してください (これには余分なオーバーヘッドが伴うため、必要な場合のみ実行します)。

ここまで試してもバグが見つからない場合は、スコープの縮小について考慮します。バイナリーハントを試してください。並列構造で if(0) を使用して並列セクションを再度シリアルにするか、プラグマをコメントアウトします。また別の方法として、並列領域の大きなチャンクをクリティカル・セクションと見なします。バグが含まれている疑いのあるコード領域を選択して、クリティカル・セクションに配置します。クリティカル・セクション内では動作し、クリティカル・セクション外では失敗するコードのセクションを探します。そして、変数を調べて、バグが明白であるかどうかを検証します。それでも動作しない場合は、コンパイラー固有の環境変数 KMP_LIBRARY=serial を設定して、プログラム全体をシリアルで実行します。

この時点でコードがまだ動作せず、OpenMP* API 関数呼び出しを使用していない場合は、[Q]openmp オプションを指定しないでコンパイルして、シリアルバージョンが動作することを確認してください。OpenMP* API 関数呼び出しを使用している場合は、[Q]openmp-stubs オプションを使用してください。

パフォーマンス

OpenMP* スレッド・アプリケーションのパフォーマンスは、次の要因に大きく依存します。

パフォーマンスの解析は常に、適切に構成された並列化アルゴリズムまたはアプリケーションから始めます。例えば、バブルソートの並列化は、手動で最適化されたアセンブリー言語であっても、良い開始位置とはいえません。スケーラビリティーに注意してください。2 個の CPU で実行するプログラムの作成は、n 個の CPU で実行するプログラムの作成よりも効率的ではありません。OpenMP* では、スレッド数はコンパイラーによって選択されます。このため、スレッド数に関係なく動作するプログラムが非常に望ましいといえます。生産/消費構造は、2 つのスレッド用に作成されているため、効率的ではありません。

アルゴリズムが決定したら、対象のインテル® アーキテクチャーでコード (シングルスレッド・バージョンが望ましい) が効率的に実行されることを確認します。[Q]openmp オプションをオフにするか、あるいは [Q]openmp-stubs オプションでビルドして、シングルスレッド・バージョンを生成し、通常の最適化を通して実行します。

シングルスレッドのパフォーマンスを確認したら、マルチスレッド・バージョンを生成して、解析を始めます。

最適化を行うには、忍耐力、経験、実践が必要です。最適化するアプリケーションと同じようにコンピューターのリソースを使用する小規模なテストプログラムを作成して、何をすると速くなるのか試してみてください。コードの並列セクションで異なる scheduling 節を試すことも忘れないでください。並列領域のオーバーヘッドが実行時間に対して大きい場合、if 節を使用してセクションをシリアルに実行すると良いかもしれません。

関連情報