正しく機能する OpenMP プログラムを作成したら、その全体のパフォーマンスを検討してみてください。OpenMP アプリケーションの効率性とスケーラビリティーを向上させる際に利用できる一般的なテクニック、および Sun プラットフォームに固有のテクニックがあります。ここでは、そうしたテクニックを簡単に説明します。
追加情報については、Darryl Gove が作成した『Solaris Application Programming』(http://www.sun.com/books/catalog/solaris_app_programming.xml) を参照してください。
また、http://developers.sun.com/sunstudio/ にある Sun の開発者向けポータルサイトもご覧ください。OpenMP アプリケーションのパフォーマンス解析と最適化に関する記事および事例研究が掲載されていることがあります。
OpenMP アプリケーションのパフォーマンスを向上させる一般的なテクニックとして、次のようなものがあります。
同期を回避する。
できる限り、BARRIER、CRITICAL 領域、ORDERED 領域、ロックの使用を回避してください。
可能な場合は NOWAIT 節を使用して、冗長または不要なバリアを取り除いてください。たとえば、並列領域の最後につねに暗黙のバリアがあります。領域の最後の DO に NOWAIT を追加することによって、1 つの冗長なバリアが取り除かれます。
名前付きの CRITICAL 領域を使用して、きめの細かいロックを行なってください。
明示的な FLUSH の使用には注意してください。フラッシュは、データキャッシュの内容をメモリーに退避させ、以降のデータアクセスで、メモリーからの再読み込みが必要になることがあります。このすべてが効率の低下になります。
デフォルトでは、アイドル状態のスレッドがある時間経過後にスリープします。デフォルトのタイムアウト期間がアプリケーションに対して不十分な場合、スレッドがスリープするのが早すぎたり、遅すぎたりすることがあります。SUNW_MP_THR_IDLE 環境変数を使用するとデフォルトのタイムアウト期間を上書きでき、アイドル状態のスレッドがスリープすることなく、常にアクティブなままにすることもできます。
外側の DO/FOR などをできる限り並列化させてください。1 つの並列領域で複数のループを囲みます。一般に、並列化のオーバーヘッドを抑制するには、並列領域をできる限り大きくします。たとえば、次のようにします。
This construct is less efficient: !$OMP PARALLEL .... !$OMP DO .... !$OMP END DO .... !$OMP END PARALLEL !$OMP PARALLEL .... !$OMP DO .... !$OMP END DO .... !$OMP END PARALLEL than this one: !$OMP PARALLEL .... !$OMP DO .... !$OMP END DO ..... !$OMP DO .... !$OMP END DO !$OMP END PARALLEL |
並列領域では、ワークシェアリング DO/FOR 指令ではなく、PARALLEL DO/FOR を使用してください。複数のループが含まれることがある一般的な並列領域よりも、PARALLEL DO/FOR を実装した方が効率的です。たとえば、次のようにします。
This construct is less efficient: !$OMP PARALLEL !$OMP DO ..... !$OMP END DO !$OMP END PARALLEL than this one: !$OMP PARALLEL DO .... !$OMP END PARALLEL |
Solaris システムでは、SUNW_MP_PROCBIND を使用してスレッドをプロセッサに結合してください。static スケジュール指定ととともにプロセッサ結合を使用すると、並列領域の前回呼び出し以降、その領域内のスレッドがアクセスするデータがローカルキャッシュに存在する、特定のデータ再利用パターンを持つアプリケーションにメリットがあります。詳細は、「2.3 プロセッサ結合」を参照してください。
可能な場所では、できる限り SINGLE ではなく、MASTER を使用してください。
MASTER 指令は、暗黙の BARRIER のない IF として実装されます。 IF(omp_get_thread_num() == 0) {...}
SINGLE 指令は、ほかのワークシェアリング構文に似た実装になります。どのスレッドが最初に SINGLE に達するかを記録するのは、実行時のオーバーヘッドの増加になります。NOWAIT が指定されていない場合、暗黙の BARRIER があります。これは効率の低下です。
適切なループスケジュール指定を選択してください。
STATIC は同期オーバーヘッドの原因にならず、データがキャッシュに収まったとき、データのローカル性を維持できます。ただし、STATIC は、負荷の不均衡をもたらすことがあります。
DYNAMIC,GUIDED は、どのチャンクが割り当てられたかを記録するため、同期オーバーヘッドを招き、そのスケジュールによってデータのローカル性の低下をもたらすことがあります。ただし、負荷均衡が改善することがあります。チャンクのサイズを変えて試してください。
オーバーヘッドが大きくなる可能性があるため、LASTPRIVATE の使用には注意してください。
並列構文からの復帰時、データを占有領域から共有領域にコピーする必要があります。
コンパイル済みのコードは、どのスレッドが論理的に最後の反復を実行したか確認します。つまり、並列 DO/FOR 内の個々の分割単位の終わりで余分な仕事が生じることになります。分割数が多いと、オーバーヘッドが増加します。
効率的なスレッドセーフのメモリー管理を使用してください。
アプリケーションが明示的に、あるいは動的/割り当て可能な配列やベクトル化された組み込み関数などのコンパイラ生成のコードで malloc() および free() が使用されていることがあります。
libc にあるスレッドセーフな malloc() および free() には、内部ブロックを原因とする大きな同期オーバーヘッドがあります。libmtmalloc ライブラリでは、より高速のバージョンが提供されています。libmtmalloc ライブラリを使用するには、リンクに -lmtmalloc を使用してください。
データが小さい場合、OpenMP の並列ループが十分に機能しないことがあります。PARALLEL 構文で IF 節を使用し、ある程度のパフォーマンス向上を期待できる場合のみ、ループを並列に実行することを指定します。
可能であれば、ループをマージしてください。たとえば、次のようにします。
2 つのループをマージ
!$omp parallel do do i = ... |
statements_1
end do !$omp parallel do do i = ... |
statements_2
end do |
1 つのループにする
!$omp parallel do do i = ... |
statements_1
statements_2
end do |
アプリケーションにある程度以上のスケーラビリティーがない場合は、入れ子並列処理を試してください。OpenMP での入れ子並列処理についての詳細は、「1.2 このマニュアルで使用している特別な表記」を参照してください。
OpenMP アプリケーションで不注意に共有メモリー構造体を使用すると、パフォーマンスおよびスケーラビリティーが低下することがあります。メモリー上の連続する共有データを複数のプロセッサが更新すると、マルチプロセッサインターコネクタに過度のトラフィックが生じ、結果的に計算の直列化の原因になることがあります。
UltraSPARC プロセッサなどの大部分の高性能プロセッサでは、低速のメモリーと CPU の高速レジスタの間にキャッシュバッファーが 1 つ挿入されています。メモリー上の場所にアクセスすると、その要求された場所を含む実際のメモリーのスライス (キャッシュライン) がキャッシュにコピーされます。同じメモリー上の場所またはその周囲の場所への以降の参照は、多くの場合、キャッシュとメモリー間の整合性を維持する必要があるとシステムが判断するまで、キャッシュから満たすことができます。
ただし、同じキャッシュライン内の個々の要素に対する、異なるプロセッサからの同時更新があると、それらの更新が互いに論理的に独立していても、キャッシュライン全体の妥当性が失われます。このため、キャッシュラインの個別要素の更新があると、その都度、そのラインには「無効」のマークが付けられます。同じ行の別の要素にアクセスしている他のプロセッサは、invalid とマークされた行を参照しています。プロセッサは、アクセスされた要素に対して変更が加えられていない場合でも、より新しい行のコピーをメモリーなどから取得するようになっています。これは、キャッシュ整合性をキャッシュラインのレベルで維持するためであり、個別の要素のためではありません。この結果、インターコネクトのトラフィックとオーバーヘッドが増加することになります。また、キャッシュラインが更新中、そのライン上の要素へのアクセスは禁止されます。
この状態は「偽りの共有」と呼ばれます。頻繁にこの状態になる場合は、OpenMP アプリケーションのパフォーマンスとスケーラビリティーが大幅に低下します。
偽りの共有によってパフォーマンスが低下するのは、次の条件のすべてが満たされる場合です。
複数のプロセッサによって共有データが変更される。
複数のプロセッサが同じキャッシュライン内のデータを更新する。
この更新が頻繁に発生する (たとえば、密なループなど)。
ループ内で読み取り専用の共有データは偽りの共有にはならないことに注意してください。
アプリケーションの実行で主要な役割を果たす並列ループを綿密に分析することによって、偽りの共有によって引き起こされるパフォーマンスおよびスケーラビリティー上の問題を明らかにすることができます。一般に、偽りの共有は次のことを行うことによって減らすことができます。
できるだけ多くの非公開データを使用する。
コンパイラの最適化機能を使用して、メモリーの読み込みおよびストア命令を取り除く。
場合によっては、大きなサイズの問題を処理しているときは共有が少ないために、偽りの共有の影響がわかりにくいことがあります。
偽りの共有を追跡するための技法は、アプリケーションによって大きく異なります。データの割り当て方法を変更すると、偽りの共有が減少する場合があります。スレッドの反復のマッピングを変更し、チャンクごとの各スレッドの作業量を増やす (chunksize 変数を変更する) ことでも、偽りの共有が減少することもあります。
Solaris 9 以降のオペレーティングシステムでは SunFireTM システム向けにスケーラビリティーとパフォーマンス向上が導入されています。中でも、MPO (Memory Placement Optimizations、メモリー配置の最適化) および MPSS (Multiple Page Size Support、複数ページサイズのサポート) が、ハードウェアのアップグレードなしに OpenMP プログラムのパフォーマンスを向上させる Solaris 9 の新機能として組み込まれました。
MPO によって、OS は、アクセスするプロセッサの近くにあるページをプロセッサに割り当てることができます。SunFire E20K および SunFire E25K システムは、同じ UniBoardTM 内と異なる UniBoard 間でメモリー待ち時間が異なります。「first-touch」というデフォルトの MPO ポリシーでは、メモリーに最初に接触するプロセッサが装着されている UniBoard 上のメモリーが割り当てられます。first-touch ポリシーは、first-touch 配置で、たいていのデータアクセスが各プロセッサにローカルのメモリーに行われるアプリケーションのパフォーマンスを大幅に改善することができます。メモリーがシステム全体に均等に分散されるランダムメモリー配置ポリシーと比較して、アプリケーションのメモリー待ち時間を短縮して帯域幅を増加することができ、その結果、パフォーマンスの向上につながります。
MPSS 機能は Solaris 9 OS リリース以降でサポートされ、プログラムが仮想メモリーの異なる領域で異なるページサイズを使用できます。Solaris のデフォルトのページサイズは比較的小さくなっています (UltraSPARC プロセッサで 8 K バイト、AMD64 Opteron プロセッサで 4 K バイト)。TLB ミスが多いと影響を受けるアプリケーションでは、大きいページサイズを使用するとパフォーマンスが向上することがあります。
TLB ミスは、Sun Performance Analyzer を使用して測定できます。
特定のプラットフォームでのデフォルトのページサイズは、Solaris OS コマンドの /usr/bin/pagesize を使用して取得できます。このコマンドで -a オプションを指定すると、サポートされるすべてのページサイズが表示されます。詳細は、pagesize(1) のマニュアルページを参照してください。
アプリケーションのデフォルトのページサイズを変更する方法は 3 つあります。
Solaris OS コマンドの ppgsz(1) を使用する
-xpagesize、-xpagesize_heap、および -xpagesize_stack の各オプション付きでアプリケーションをコンパイルする。詳細は、コンパイラのマニュアルページを参照してください。
MPSS 固有の環境変数を使用する。詳細は、mpss.so.1(1) のマニュアルページを参照してください。