OpenMP* の高度な問題

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

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

関数呼び出しを使用するには、omp.h ヘッダーファイルと omp_lib.h ヘッダーファイルをインクルードし (コンパイラーのインストール時に、INCLUDE ディレクトリーにインストールされます)、-openmp (Linux* および Mac OS* X) オプションまたは /Qopenmp (Windows*) オプションを使用してアプリケーションをコンパイルします。

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

最初に、プラグマの代わりに関数を使用するには、コードを書き換える必要があります。コードの書き換えには、追加のデバッグ、テスト、メンテナンスが伴います。

また、OpenMP サポートなしにコンパイルすることは困難です。

次のループではスレッド数が 26 の倍数でない場合、アルファベットの文字はすべて出力されません。このように、単純なバグは簡単に引き起こされます。

最後に、ワークキューのアルゴリズムを独自に作成しない限り、ループ・スケジューリングの調整ができなくなります。ワークキューのアルゴリズムを独自に作成する場合、一般的には例に示すような 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 is not a multiple of 26,

    // which can be considered a bug in this code.

    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 は、private 変数、共有変数、および追加コードを挿入するため、OpenMP をサポートする専用のデバッガーなしでは、検証しステップ実行することは不可能です。OpenMP を使用する場合、主要なデバッグツールは排除処理を行います。

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

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

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

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

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

この時点でコードがまだ動作しない場合は、-openmp (Linux および Mac OS X) または /Qopenmp (Windows) オプションを指定しないでコンパイルして、シリアルバージョンが動作することを確認してください。

パフォーマンス

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

マルチスレッド・コードのパフォーマンスは、2 つの要因に影響されます。

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

アルゴリズムが決定したら、対象のインテル(R) アーキテクチャーでコード (シングルスレッド・バージョンが望ましい) が効率的に実行されることを確認します。-openmp (Linux および Mac OS X) または /Qopenmp (Windows) オプションをオフにして、シングルスレッド・バージョンを生成し、通常の最適化を通して実行します。詳細は、「OpenMP* を使用したワークシェアリング」を参照してください。

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

最初に、オペレーティング・システムのアイドルループにかかった時間を検証します。インテル(R) VTune(TM) パフォーマンス・アナライザーやインテル(R) スレッド化解析ツールは調査に役立つ便利なツールです。アイドル時間は、不均衡な負荷、大量のブロックされた同期、シリアル領域を示します。これらの問題を解決して、VTune(TM) アナライザーに戻り、過度なキャッシュミスおよびフォルス・シェアリングなどのメモリー問題を調査します。これらの基本的な問題を解決すると、デュアルコア・プロセッサーおよびマルチコア・プロセッサー、ハイパースレッディング・テクノロジーや複数の物理プロセッサーでの実行に適した、最適化された並列プログラムが完成します。

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