高度な OpenMP* プログラミング

同カテゴリーの次の記事

OpenMP* を使用したその他のワークシェア

この記事は、インテル® ソフトウェア・ネットワークに掲載されている「Advanced OpenMP* Programming」の日本語参考訳です。


はじめに

この記事は、経験豊富な C/C++ プログラマー向けに、OpenMP* を使ってアプリケーションのスレッドの生成、同期、終了を単純化する方法を紹介するシリーズ (全 3 つ) の 3 つ目です。

  • 1 つ目の記事「OpenMP* 入門」では、OpenMP* の最も一般的な機能である、ループのワークシェアを紹介します。
  • 2 つ目の記事「OpenMP* を使用したその他のワークシェア」では、ループ以外の並列化とその他の一般的な OpenMP* 機能の活用方法について説明します。
  • 最後の記事「高度な OpenMP* プログラミング」では、OpenMP* ランタイム・ライブラリーについて説明し、問題が発生した場合にアプリケーションをデバッグする方法も紹介します。

ランタイム・ライブラリー関数

ご存知のように、OpenMP* は、プラグマ、ランタイム・ライブラリー関数呼び出し、環境変数のセットです。最初の 2 つの記事でプラグマについて説明し、この最後の記事でランタイム・ライブラリー関数呼び出しと環境変数について説明します。この構成にした理由は明らかです。プラグマは、高度な機能を容易に提供する、OpenMP* の最も重要な要素であるためです。使用にあたってソースコードの構造を変更する必要はなく、プラグマを無視するだけでシリアルバージョンを生成することができます。

一方、ランタイム・ライブラリー関数呼び出しを使用する場合はプログラムを変更する必要があるため、単純にシリアルバージョンを生成することは困難です。よく分からない場合は、常にプラグマを使用し、ランタイム・ライブラリー関数呼び出しは使用しないようにしてください。関数呼び出しを使用する場合は、ヘッダーファイルをインクルードして、インテル® C++ コンパイラーのコマンドライン・オプション /Qopenmp を指定します。別途ライブラリーをリンクする必要はありません。

下記の表に示す 4 つのライブラリー関数 (スレッド数を返す、スレッド数を設定する、現在のスレッド番号を返す、利用可能なプロセッサー数を返す) が最も頻繁に使用されます。OpenMP* ライブラリー関数の完全なリストは、OpenMP* Web サイト (www.openmp.org) でご覧になれます。

int omp_get_num_threads(void); 利用可能なスレッド数を返します。並列領域の外で呼び出された場合は 1 を返します。
int omp_set_num_threads(int NumThreads) 並列セクションで使用するスレッド数を設定します。OMP_NUM_THREADS 環境変数よりも優先されます。
int omp_get_thread_num(void); 任意のスレッド番号を 0 (マスタースレッド) から合計スレッド数 -1 の間で返します。
int omp_get_num_procs(void); 利用可能なプロセッサー数を返します。ハイパースレッディング・テクノロジー (HT テクノロジー) 対応プロセッサーは 2 つの論理プロセッサーとしてカウントされます。

下記の例は、これらの関数を使用してアルファベットを出力しています。

omp_set_num_threads(4);

#pragma omp parallel private(i)

{   // このコードにはバグがあります。分かりますか?

    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);

}

上記の例は、ライブラリー関数呼び出しを使用する際の重要な概念をいくつか示しています。まず最初に、コードを書き換える必要があります。コードの書き換えには、仕様書の変更、デバッグ、テスト、メンテナンスが伴います。さらに OpenMP* サポートなしにコンパイルすることはできません。また、コード変更でバグが混入する可能性もあります。上記のループでは、スレッド数が 26 の倍数でない場合、アルファベットの文字はすべて出力されません。そして最後に、ワークキューのアルゴリズムを独自に作成しない限り、ループのスケジュールを調整できません。通常は、例に示したスタティック・スケジュールのような独自の割り当てによって制限されることになります。


環境変数

OpenMP* 仕様では、次に示す環境変数が定義されています。

環境変数 説明
OMP_SCHEDULE for ループのワークシェア構造のスケジュールを制御します。 set OMP_SCHEDULE="guided, 2"
OMP_NUM_THREADS デフォルトのスレッド数を設定します。omp_set_num_threads() 関数呼び出しは、この値よりも優先されます。 set OMP_NUM_THREADS=4

また、このほかにコンパイラー固有の環境変数を利用できます。詳細は、コンパイラーのドキュメントを参照してください。


デバッグ

デバッガーを利用してもランタイム時の再現性の問題から競合状態が発生しないことがあるため、マルチスレッド・アプリケーションのデバッグは非常に困難です。print 文は同期とオペレーティング・システム関数を使用するため、print 文によって問題が発見しにくくなることもあります。OpenMP* を利用するとさらに複雑になります。OpenMP* はプライベート変数、共有変数、追加コードを挿入するため、OpenMP* をサポートする専用のデバッガーなしでは、ステップ実行して検証することはできません。そのため、排除処理が重要になります。

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

下記に示す default(none) 節は、発見が困難な変数を探すのに役立ちます。default(none) を指定する場合、各変数はデータ共有属性節とともに宣言する必要があります。宣言がないとエラーになるため、発見が容易です。

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

shared(a,b)

もう 1 つの一般的なミスは、初期化されていない変数の使用です。プライベート変数は、並列構造の入口では初期値を持っていないため、 firstprivate 節を使用して変数を初期化します (オーバーヘッドが伴うため、必要な場合のみ実行してください)。もしくは、lastprivate 節を使用して並列領域で求めた結果を、その後のシリアル処理へ受け継ぎます。

それでもまだバグが発見できない場合、 調べているコードの範囲が広すぎる可能性があります。コードの切り分けを試してください。並列構造で if(0) を使用して並列セクションを再度シリアルにするか、プラグマをコメントアウトします。別の方法として、大きな並列領域をクリティカル・セクションと見なします。バグが含まれている疑いのあるコード領域を選択して、クリティカル・セクションにします。クリティカル・セクション内では動作し、クリティカル・セクション外では失敗するコードのセクションを探します。変数を調べて、バグがあるかどうかを検証してください。それでも動作しない場合は、インテル® コンパイラー固有の環境変数 KMP_LIBRARY = serial を設定して、プログラム全体をシリアルで実行します。

この時点でコードがまだ動作しない場合は、/Qopenmp オプションを指定せずにコンパイルして、シリアルバージョンが動作することを確認します。


インテル® Inspector XE (旧インテル® スレッド・チェッカー)

デバッガーや lint ツールとは異なるインテル® Inspector XE は、重要な並列実行情報とデバッグのヒントを提供します。コーディング・エラーを識別するため、ソースコードまたはバイナリー・インストルメンテーションを使用して、OpenMP* プラグマ、Win32 スレッド API、すべてのメモリーアクセスをモニターします。テスト中には発生しないのに、顧客サイトでは常に発生するような再現性が確実でないエラーも検出することができます。

このツールを使用するときは、データ収集プロセスに時間がかかるため、最小限のメモリーですべてのコードパスにアクセスすることが重要です。解析時間を短縮するには、アプリケーションで処理するデータ量を減らすようにソースコードやデータセットを少し変更する必要があります。

インテル® Inspector XE の詳細は、インテル® ソフトウェア開発製品 Web サイトを参照してください。


パフォーマンス

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

  • 基本となるシングルスレッド・コードのパフォーマンス。
  • CPU 稼働率、アイドルスレッド、ロードバランス。
  • 並列実行されるアプリケーションの比率。
  • スレッド間の同期と通信の量。
  • スレッドの生成、管理、終了、同期に伴うオーバーヘッド。fork-join によるシングルから並列への切り替え (single-to-parallel)、または並列からシングルへの切り替え (parallel-to-single) によって増加します。
  • メモリーの帯域幅、バスの帯域幅、CPU 実行ユニットなど、共有リソースのパフォーマンス制約。
  • 共有メモリーまたはフォルスシェアによって生じるメモリーの競合。

スレッドコードのパフォーマンスは、主に 2 つの要素 (シングルスレッド・バージョンが適切に実行されるか、オーバーヘッドが最小限になるようにワークがプロセッサー間で適切に分割されているか) によって決定されます。パフォーマンスの解析は、適切に設計された並列化アルゴリズムまたはアプリケーションから始めます。例えば、バブルソートの並列化は、手動で最適化されたアセンブリー言語で記述されていても、最初に対象とすべきアプリケーションではないことは明白です。

また、スケーラビリティーにも注意してください。2 個の CPU で実行するプログラムの作成は、n 個の CPU で実行するプログラムの作成よりも効率的ではありません。OpenMP* ではスレッド数はコンパイラーによって選択されるため、スレッド数に関係なく動作するプログラムが非常に望ましいといえます。生産者/消費者モデルは、2 つのスレッド用に作成されているため、効率的ではありません。

アルゴリズムが決定したら、インテル® アーキテクチャーでコード (シングルスレッド・バージョンが望ましい) が効率的に実行されることを確認します。OpenMP* コンパイラー・オプションをオフにしてシングルスレッド・バージョンを生成し、通常の最適化セットを利用して実行することができます。シングルスレッドの最適化については、『The Software Optimization Cookbook、2nd Edition』 (英語) が非常に参考になります。シングルスレッドのパフォーマンスを確認したら、マルチスレッド・バージョンを生成して、解析を始めます。

最初に、オペレーティング・システムのアイドルループで費やされている時間を調べます。この調査には、インテル® VTune™ Amplifier XE を利用すると便利です。アイドル時間は、アンバランスなロード、大量のブロックされた同期、シリアル領域を示します。これらの問題を修正した後、インテル® VTune™ Amplifier XE に戻って、過度のキャッシュミスおよびフォルスシェアのようなメモリー固有の問題を調べます。これらの基本的な問題を解決すれば、ハイパースレッディング・テクノロジーでも複数の物理プロセッサーでも適切に動作する高度に最適化された並列プログラムを生成することができます。

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

#pragma omp parallel for if(NumBytes > 50)

このシリーズでは OpenMP* の概要について説明しているため、パフォーマンスを最適化する際の詳細なアプローチに関する説明は含まれていません。パフォーマンスの最適化についての説明は、インテル® ソフトウェア・ネットワーク Web サイト (英語) および iSUS サイト に掲載されている記事を参照してください。「関連情報」セクションにいくつかの記事をリストしています。


インテル® VTune™ Amplifier XE (旧インテル® スレッド・プロファイラー)

インテル® VTune™ Amplifier XE は、アプリケーションのマルチスレッド・パフォーマンスを視覚化します。並列セクションとシリアルセクションで費やされている時間、オーバーヘッド、同期、その他の情報が表示されます。OpenMP* プラグマの代わりにパフォーマンス測定とモニタリングが行われ、データが記録されます。インテル® VTune™ Amplifier XE の詳細は、インテル® ソフトウェア開発製品 Web サイトを参照してください。


まとめ

OpenMP* は、アプリケーションのどこを、どのようにスレッド化するかコンパイラーに明示的に指示する、プラグマ、ランタイム・ライブラリー関数呼び出し、環境変数の柔軟かつ単純なセットです。OpenMP* の機能を活用することにより、マルチスレッド・プログラミングがシングルスレッド・プログラミングよりも困難であるという常識を覆すことができます。皆さんも是非スレッド化に挑戦してみてください。

OpenMP* の詳細は、OpenMP* 仕様 (http://www.openmp.org) を参照してください。


このシリーズのほかの記事

関連情報


コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

関連記事