Sun Studio 12: パフォーマンスアナライザ

OpenMP ソフトウェアの実行の概要

OpenMP アプリケーションの実際の実行モデルについては、OpenMP の仕様 (たとえば、OpenMP Application Program Interface, Version 2.5、1.3 節などを参照) で説明されています。しかし、仕様には、ユーザーにとって重要と思われるいくつかの実装の詳細が説明されていません。また、Sun での実際の実装では、直接記録されたプロファイリング情報からユーザーがスレッド間の相互作用を簡単に理解できないことがわかっています。

ほかのシングルスレッドプログラムが実行されるとき同様に、呼び出しスタックが現在位置と、どのようにしてそこまで到達したかのトレースを、ルーチン内の _start と呼ばれる冒頭の命令を始めとして表示します。このルーチンは main を呼び出し、その後、main によって処理が進められ、プログラム内のさまざまなサブルーチンが呼び出されます。サブルーチンにループが含まれている場合、プログラムは、ループ終了条件が満たされるまでループ内のコードを繰り返し実行します。その後、実行は次のコードシーケンスへ進み、以後同様に処理が続きます。

プログラムが OpenMP (または、自動並列化処理) によって並列化されると、動作は異なります。この動作の直感的なモデルでは、メインスレッド (マスタースレッド) が、シングルスレッドプログラムとまったく同じように実行されます。並列ループまたは並列領域に到達すると、追加のスレーブスレッドが出現します。それらの各スレッドはマスタースレッドのクローンであり、それらのスレッドすべてが、ループまたは並列領域のコンテンツを互いに異なる作業チャンク用に並列実行します。すべての作業チャンクが完了すると、すべてのスレッドの同期がとられ、スレーブスレッドが消失し、マスタースレッドが処理を続行します。

コンパイラが並列領域またはループ用のコード (または、その他の任意の OpenMP 構造) を生成するとき、それらの内部のコードが抽出され、mfunction と呼ばれる独立した関数が作成されます。この関数は、アウトライン関数、またはループ本体関数とも呼ばれます。関数の名前は、OpenMP 構造タイプ、抽出元となった関数の名前、その構造が置かれているソース行の行番号を符号化したものです。これらの関数の名前は、アナライザ内では次の形式で表示されます。ここで、大括弧内の名前は関数の実際のシンボルテーブル名です。

bardo_ -- 行 9 からの OMP 並列領域 [_$p1C9.bardo_]
atomsum_ -- 行 7 からの MP doall [_$d1A7.atomsum_]

これらの関数には、ほかのソース構造から生成される別の形式もあり、その場合、名前の中の「OMP 並列領域」は、「MP コンストラクト」、「MP doall」、「OMP 領域」のいずれかに置き換えられます。このあとの説明では、これらすべてを「並列領域」と総称します。

並列ループ内のコードを実行する各スレッドは、mfunction を複数回呼び出すことができ、1 回呼び出すたびにループ内の 1 つの作業チャンクが実行されます。すべての作業チャンクが完了すると、それぞれのスレッドはライブラリ内の同期ルーチンまたは縮小ルーチンを呼び出します。その後、マスタースレッドが続行される一方、各スレーブスレッドはアイドル状態になり、マスタースレッドが次の並列領域に入るまで待機します。すべてのスケジューリングと同期は、OpenMP ランタイムの呼び出しによって処理されます。

並列領域内のコードは、その実行中、作業チャンクを実行しているか、ほかのスレッドとの同期をとっているか、行うべき追加の作業チャンクを取り出している場合があります。また、ほかの関数を呼び出す場合もあり、それによってさらに別の関数が呼び出される可能性もあります。並列領域内で実行されるスレーブスレッド (またはマスタースレッド) は、それ自体が、またはそれが呼び出す関数から、マスタースレッドとして動作し、独自の並列領域に入って入れ子並列を生成する場合があります。

アナライザは、呼び出しスタックの統計的な標本収集に基づいてデータを収集し、すべてのスレッドにまたがってデータを集計し、収集したデータのタイプに基づき、関数、呼び出し元と呼び出し先、ソース行、および命令を対象にパフォーマンスのメトリックを表示します。アナライザは、OpenMP プログラムのパフォーマンスに関する情報を、ユーザーモードとマシンモードという 2 つのモードのいずれかで提示します。第 3 のモードとして、上級モードがサポートされていますが、これはユーザーモードと同じものです。

詳細は、OpenMP のユーザーコミュニティーにあるホワイトペーパー『An OpenMP Runtime API for Profiling』を参照してください。

OpenMP プロファイルデータのユーザーモードの表示

プロファイルデータのユーザーモードの表示では、プログラムが実際に 「OpenMP ソフトウェアの実行の概要」で説明されているモデルに従って実行されたかのように情報が提示されます。実際のデータは、ランタイムライブラリ libmtsk.so の実装の詳細を取り込んだもので、これは、モデルに対応していません。ユーザーモードでは、プロファイルデータの表示はモデルにさらに近くなるよう変更されるため、記録されたデータやマシンモードの表示と次の 3 つの点で異なっています。

擬似関数

擬似関数は、スレッドが OpenMP ランタイムライブラリ内の何らかの状態にあったイベントを反映するために構築され、ユーザーモード呼び出しスタック上に置かれます。

定義されている擬似関数と、その機能の説明を示します。

スレッドが、これらの関数の 1 つに対応する OpenMP ランタイム状態にあるとき、対応する関数がスタック上にリーフ関数として追加されます。スレッドのリーフ関数は、OpenMP ランタイム内のどこかにある場合には、リーフ関数として <OMP オーバーヘッド> によって置き換えられます。そうでない場合、OpenMP ランタイムに入っているすべての PC は、ユーザーモードスタックから除外されます。

ユーザーモード呼び出しスタック

このモデルを理解するもっとも簡単な方法は、OpenMP プログラムの呼び出しスタックを、そのプログラムの実行のさまざまな時点で見ることです。この節では、1 つのサブルーチン foo を呼び出す main プログラムを持つ単純なプログラムについて考えます。そのサブルーチンには単一の並列ループがあり、その中でいくつかのスレッドが作業を行い、ロックを求めて競合し、ロックの取得と解放を行い、critical セクションへの出入りを行います。追加の呼び出しスタックセットが示され、そこに 1 つのスレーブスレッドが別の関数 bar を呼び出したときの状態が反映されます。bar 関数は入れ子になった並列領域に入ります。

この表示では、並列領域内で費やされたすべての包括的時間が、抽出元となった関数内の包括的時間に含まれ、それには、OpenMP ランタイムで費やされた時間も含まれており、その包括的時間は main および _start まで伝搬されます。

このモデルの動作を表す呼び出しスタックは、このあとのサブセクションに示すような外観となります。並列領域関数の実際の名前は、前に説明したように、次の形式をとります。

foo -- 行 9 からの OMP 並列領域 [ [_$p1C9.foo]
bar -- 行 5 からの OMP 並列領域 [ [_$p1C5.bar]

わかりやすくするために、説明の中では次のように短縮した形式が使用されています。

foo -- OMP...
bar -- OMP...

説明の中で、プログラム実行中の、ある瞬間におけるすべてのスレッドからの呼び出しスタックが示されています。各スレッドの呼び出しスタックは、フレームのスタックとして示されており、個々のプロファイルイベントを単一スレッドに関するアナライザの「タイムライン」タブで選択した際のデータに一致し、リーフ PC が一番上になっています。「タイムライン」タブでは、各フレームの PC オフセットが表示されますが、ここでは省略されています。すべてのスレッドからのスタックが、水平方向の配列で示されています。アナライザの「タイムライン」タブでは、ほかのスレッドのスタックが、垂直方向に積まれたプロファイルバー内に表示されます。さらに、示されている表現の中では、すべてのスレッドのスタックが正確に同じ瞬間に取得されたかのように示されていますが、実際の実験では、各スタックはそれぞれのスレッド内で独立して取得され、互いに相対的なずれが存在する場合があります。

示されている呼び出しスタックは、アナライザまたは er_print ユーティリティーにおける「ユーザー」の表示モードで提示されるとおりのデータを表しています。

  1. 最初の並列領域の前

    最初の並列領域に入る前の時点で存在するスレッドは、マスタースレッドただ 1 つだけです。

    マスター

    foo

    main

    _start

  2. 最初の並列領域に入った時点

    この時点では、ライブラリがスレーブスレッドの作成を完了し、すべてのスレッドは、マスターもスレーブもそれぞれの作業チャンクの処理を開始しようとしています。すべてのスレッドは、並列領域用のコード foo-OMP... を その構造の OpenMP 指令が現れる foo から、または自動並列化されたループ文を含んでいる行から呼び出したものとして示されます。各スレッド内の並列領域用のコードは、並列領域内の最初の命令から <OMP オーバーヘッド> 関数として示されている OpenMP サポートライブラリを呼び出しています。

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    <OMP オーバーヘッド>

    <OMP オーバーヘッド>

    <OMP オーバーヘッド>

    <OMP オーバーヘッド>

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo

    foo

    foo

    foo

    main

    main

    main

    main

    _start

    _start

    _start

    _start

    <OMP オーバーヘッド> が現れる枠は小さいので、この関数は特定の実験に現れない場合があります。

  3. 並列領域の内部で実行中

    4 つのスレッドすべてが、並列領域内で有益な作業を実行しています。

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo

    foo

    foo

    foo

    main

    main

    main

    main

    _start

    _start

    _start

    _start

  4. 並列領域内で作業チャンク間を実行中

    4 つのスレッドすべてが有益な作業を行なっていますが、1 つのスレッドが 1 つの作業チャンクを終了し、次のチャンクを取得しようとしています。

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

     

    <OMP オーバーヘッド>

       

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo

    foo

    foo

    foo

    main

    main

    main

    main

    _start

    _start

    _start

    _start

  5. 並列領域内の critical セクションを実行中

    4 つのスレッドすべてが、それぞれ並列領域内で実行中です。スレッドの 1 つが critical セクション内にある一方、別の 1 つは critical セクションに到達する前 (またはそれを終了したあと) に実行中です。残りの 2 つは、critical セクションに入るのを待っています。

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    <OMP critical セクション待ち>

       

    <OMP critical セクション待ち>

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo

    foo

    foo

    foo

    main

    main

    main

    main

    _start

    _start

    _start

    _start

    収集されたデータからは、critical セクション内で実行中のスレッドの呼び出しスタックと、まだ到達していないスレッド、またはすでに critical セクションを通過したスレッドの呼び出しスタックを区別できません。

  6. 並列領域内でロックを迂回して実行中

    ロックを迂回するコードのセクションは、critical セクションとほとんど同じです。4 つのスレッドすべてが並列領域内で実行中です。1 つのスレッドがロックを保持しながら実行中で、1 つはロックを取得する前 (または取得して解放したあと) に実行中で、それ以外の 2 つのスレッドはロックを待っています。

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    <OMP ロック待ち>

       

    <OMP ロック待ち>

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo

    foo

    foo

    foo

    main

    main

    main

    main

    _start

    _start

    _start

    _start

    critical セクションの例のように、収集されたデータからは、ロックを保持して実行中のスレッドの呼び出しスタックと、ロックを取得する前または解放したあとに実行中のスタックを区別できません。

  7. 並列領域の終わり近く

    この時点では、3 つのスレッドがすべてその作業チャンクを終了しましたが、1 つのスレッドがまだ作業中です。この例の OpenMP 構造では、暗黙にバリアが指定されました。バリアがユーザーコードによって明示的に指定されていたとすると、<OMP 暗黙バリア> 関数は <OMP 明示バリア> によって置き換えられます。

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    <OMP 暗黙バリア>

    <OMP 暗黙バリア>

     

    <OMP 暗黙バリア>

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo

    foo

    foo

    foo

    main

    main

    main

    main

    _start

    _start

    _start

    _start

  8. 並列領域の終わり近くで、1 つ以上の縮約変数がある

    この時点では、2 つのスレッドがそのすべての作業チャンクを終了し、縮約計算を実行中ですが、1 つのスレッドはまだ作業中であり、4 番目のスレッドはすでに縮約の一部を終了し、バリアの位置で待機中です。

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    <OMP 縮約>

    <OMP 暗黙バリア>

     

    <OMP 暗黙バリア>

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo

    foo

    foo

    foo

    main

    main

    main

    main

    _start

    _start

    _start

    _start

    1 つのスレッドが <OMP 縮約> 関数内として示されていますが、縮約を行うために費やされる実際の時間はかなり短いのが普通で、呼び出しスタックの標本内に取得されることは、ほとんどありません。

  9. 並列領域の終わり

    この時点では、すべてのスレッドが並列領域内でのすべての作業チャンクを完了し、バリアに到達しています。

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    <OMP 暗黙バリア>

    <OMP 暗黙バリア>

    <OMP 暗黙バリア>

    <OMP 暗黙バリア>

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo

    foo

    foo

    foo

    main

    main

    main

    main

    _start

    _start

    _start

    _start

    すべてのスレッドがすでにバリアに到達しているので、それらはすべて先へ進むことができます。実験ですべてのスレッドがこの状態にあるのが見られることは、ほとんどありません。

  10. 並列領域を出たあと

    この時点では、すべてのスレーブスレッドが次の並列領域への進入を待っており、ユーザーが設定した各種の環境変数に応じて、スピン中またはスリープ中になります。プログラムは、逐次実行中です。

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    foo

         

    main

         

    _start

    <OMP アイドル>

    <OMP アイドル>

    <OMP アイドル>

  11. 入れ子の並列領域を実行中

    4 つのスレッドすべてが、それぞれ並列領域内で作業中です。1 つのスレーブスレッドが別の関数 bar を呼び出し、それによって入れ子の並列領域が作成され、それを処理するために追加のスレーブスレッドが作成されます。

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    スレーブ 4 

     

    bar-OMP...

       

    bar-OMP...

     

    bar

       

    bar

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo-OMP...

    foo

    foo

    foo

    foo

    foo

    main

    main

    main

    main

    main

    _start

    _start

    _start

    _start

    _start

OpenMP のメトリック

OpenMP プログラムの時間プロファイルイベントを処理するときは、OpenMP システム内の 2 つの状態で個々に費やされた時間に対応する 2 つのメトリックが示されます。それらは、「OMP ワーク」と「OMP 待ち」です。

スレッドがユーザーコードから実行されたときは、逐次か並列かを問わず、「OMP ワーク」に時間が累積されます。スレッドが何かを待って先へ進めずにいるときは、その待機が busy-wait (spin-wait) であるかスリープ状態であるかを問わず、「OMP 待ち」に時間が累積されます。これら 2 つのメトリックの合計は、時間プロファイル内の「合計 LWP 時間」メトリックに一致します。

OpenMP プロファイルデータのマシン表現

プログラムの実行のさまざまな局面における実際の呼び出しスタックは、前述の直感的なモデルに示したものとは大きく異なります。マシンモードの表現では、呼び出しスタックが測定どおりに表示され、変換は行われず、擬似関数も構築されません。ただし、時間プロファイルのメトリックは依然として示されます。

次に示す各呼び出しスタックでは、libmtsk は OpenMP ランタイムライブラリ内の呼び出しスタックに入っている 1 つ以上のフレームを表しています。どの関数がどの順序で表示されるかの詳細は、バリア用のコードまたは縮小を行うコードの内部的な実装と同様に、リリースによって異なります。

  1. 最初の並列領域の前

    最初の並列領域に入る前の時点で存在するスレッドは、マスタースレッドただ 1 つだけです。呼び出しスタックはユーザーモードの場合と同じです。

    マスター 

    foo

    main

    _start

  2. 並列領域内で実行中

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    foo-OMP...

         

    libmtsk

         

    foo

    foo-OMP...

    foo-OMP...

    foo-OMP...

    main

    libmtsk

    libmtsk

    libmtsk

    _start

    _lwp_start

    _lwp_start

    _lwp_start

    マシンモードでは、スレーブスレッドはマスターが開始された _start 内ではなく、_lwp_start 内で開始されたものとして示されます。一部のバージョンのスレッドライブラリでは、この関数は _thread_start として表示されます。

  3. すべてのスレッドがバリアの位置にある

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    libmtsk

         

    foo-OMP...

         

    foo

    libmtsk

    libmtsk

    libmtsk

    main

    foo-OMP...

    foo-OMP...

    foo-OMP...

    _start

    _lwp_start

    _lwp_start

    _lwp_start

    スレッドが並列領域内で実行されるときと異なり、スレッドがバリアの位置で待機しているときは、foo と並列領域コード foo-OMP... の間に OpenMP ランタイムからのフレームは存在しません。その理由は、実際の実行には OMP 並列領域関数が含まれていませんが、OpenMP ランタイムがレジスタを操作し、スタック展開で直前に実行された並列領域関数からランタイムバリアコードへの呼び出しが示されるようにするからです。そうしないと、どの並列領域がバリア呼び出しに関連しているかをマシンモードで判定する方法がなくなってしまいます。

  4. 並列領域から出たあと

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    foo

         

    main

    libmtsk

    libmtsk

    libmtsk

    _start

    _lwp_start

    _lwp_start

    _lwp_start

    スレーブスレッド内では、呼び出しスタック上にユーザーフレームが存在しません。

  5. 入れ子の並列領域内にいるとき

    マスター 

    スレーブ 1 

    スレーブ 2 

    スレーブ 3 

    スレーブ 4 

     

    bar-OMP...

         

    foo-OMP...

    libmtsk

         

    libmtsk

    bar

         

    foo

    foo-OMP...

    foo-OMP...

    foo-OMP...

    bar-OMP...

    main

    libmtsk

    libmtsk

    libmtsk

    libmtsk

    _start

    _lwp_start

    _lwp_start

    _lwp_start

    _lwp_start