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

呼び出しスタックとプログラムの実行

呼び出すスタックは、プログラム内の命令を示す一連のプログラムカウンタ (PC) のアドレスです。リーフ PC と呼ばれる最初の PC はスタックの一番下に位置し、次に実行する命令のアドレスを表します。次の PC はそのリーフ PC を含む関数の呼び出しアドレス、そして、その次の PC がその関数の呼び出しアドレスというようにして、これがスタックの先頭まで続きます。こうしたアドレスはそれぞれ、復帰アドレスと呼びます。呼び出しスタックの記録では、プログラムスタックから復帰アドレスが取得されます。これは、「スタックの展開」と呼ばれています。展開の失敗については、「不完全なスタック展開」を参照してください。

呼び出しスタック内のリーフ PC は、この PC が存在する関数にパフォーマンスデータの排他的メトリックを割り当てるときに使用されます。リーフ PC を含むスタック上の各 PC は、その PC が存在する関数に包括的メトリックを割り当てるときに使用されます。

ほとんどの場合、記録された呼び出しスタック内の PC は、プログラムのソースコードに現れる関数に自然な形で対応しており、パフォーマンスアナライザが報告するメトリックもそれらの関数に直接対応しています。しかし、プログラムの実際の実行は、単純で直観的なプログラム実行モデルと対応しないことがあり、その場合は、アナライザの報告するメトリックが紛らわしいことがあります。こうした事例については、「プログラム構造へのアドレスのマッピング」を参照してください。

シングルスレッド実行と関数の呼び出し

プログラムの実行でもっとも単純なものは、シングルスレッドのプログラムがそれ専用のロードオブジェクト内の関数を呼び出す場合です。

プログラムがメモリーに読み込まれて実行が開始されると、初期実行アドレス、初期レジスタセット、スタック (スクラッチデータの格納および関数の相互の呼び出し方法の記録に使用されるメモリー領域) からなるコンテキストが作成されます。初期アドレスは常に、あらゆる実行可能ファイルに組み込まれる _start() 関数の先頭位置になります。

プログラムを実行すると、たとえば関数呼び出しや条件文を表すことがある分岐命令があるまで、命令が順実行されます。分岐点では、分岐先が示すアドレスに制御が渡されて、そこから実行が続行されます。通常、分岐の次の命令は実行されるようにコミットされています。この命令は、分岐遅延スロット命令と呼ばれます。ただし、分岐命令には、この分岐遅延スロット命令の実行を無効にするものもあります。

呼び出しを表す命令シーケンスが実行されると、復帰アドレスがレジスタに書き込まれ、呼び出された関数の最初の命令から実行が続行されます。

ほとんどの場合は、この呼び出し先の関数の最初の数個の命令のどこかで、新しいフレーム (関数に関する情報を格納するためのメモリー領域) がスタックにプッシュされ、そのフレームに復帰アドレスが格納されます。復帰アドレスに使用されるレジスタは、呼び出された関数がほかの関数を呼び出すときに使用できます。関数から制御が戻されようとすると、スタックからフレームがポップされ、関数の呼び出し元のアドレスに制御が戻されます。

共有オブジェクト間の関数の呼び出し

共有オブジェクト内の関数が別の共有オブジェクトの関数を呼び出す場合は、同じプログラム内の単純な関数の呼び出しよりも実行が複雑になります。各共有オブジェクトには、プログラムリンケージテーブル (PLT) が 1 つあり、その PLT には、そのオブジェクトが参照する関数で、そのオブジェクトの外部にあるすべての関数 (外部関数) のエントリが含まれます。最初は、PLT 内の各外部関数のアドレスは、実際には動的リンカーである ld.so 内のアドレスです。外部関数が初めて呼び出されると、制御が動的リンカーに移り、動的リンカーは、その外部関数への呼び出しを解決し、以降の呼び出しのために、PLT のアドレスにパッチを適用します。

3 つの PLT 命令の中の 1 つを実行しているときにプロファイリングイベントが発生した場合、PLT PC は削除され、排他的時間はその呼び出し命令に対応することになります。PLT エントリによる最初の呼び出し時にプロファイリングイベントが発生し、かつリーフ PC が PLT 命令ではない場合、ld.so のコードと PLT が起因する PC はすべて、包括的時間を集計する擬似的な関数 @plt の呼び出しと置き換えられます。 各共有オブジェクトには、こういった擬似的な関数が 1 つ用意されています。LD_AUDIT インタフェースを使用しているプログラムの場合、PLT エントリが絶対にパッチされない可能性があるとともに、@plt の非リーフ PC の発生頻度が高くなることが考えられます。

シグナル

シグナルがプロセスに送信されると、さまざまなレジスタ操作とスタック操作が発生し、シグナル送信時のリーフ PC が、システム関数 sigacthandler() への呼び出しの復帰アドレスを示していたかのように見えます。sigacthandler() は、関数が別の関数を呼び出すのと同じようにして、ユーザー指定のシグナルハンドラを呼び出します。

パフォーマンスアナライザは、シグナル送信で発生したフレームを通常のフレームとして処理します。シグナル送信時のユーザーコードがシステム関数 sigacthandler() の呼び出し元として表示され、sigacthandler() がユーザーのシグナルハンドラの呼び出し元として表示されます。sigacthandler() とあらゆるユーザーシグナルハンドラ、さらにはそれらが呼び出すほかの関数の包括的メトリックは、割り込まれた関数の包括的メトリックとして表示されます。

コレクタは sigaction() に割り込むことによって、時間データ収集時にはそのハンドラが SIGPROF シグナルのプライマリハンドラになり、ハードウェアカウンタオーバーフローのデータ収集時には SIGEMT シグナルのプライマリハンドラになるようにします。

トラップ

トラップは命令またはハードウェアによって発行され、トラップハンドラによって捕捉されます。システムトラップは、命令から発行され、カーネルにトラップされるトラップです。すべてのシステムコールは、トラップ命令を使用して実装されます。ハードウェアトラップの例としては、命令 (UltraSPARC® III プラットフォームでのレジスタ内容値の fitos 命令など) を最後まで実行できないとき、あるいは命令がハードウェアに実装されていないときに、浮動小数点演算装置から発行されるトラップがあります。

トラップが発行されると、Solaris LWP または Linux カーネルはシステムモードになります。Solaris OS 上では、通常、これでマイクロステートはユーザー CPU 状態からトラップ状態、そしてシステム状態に切り替わります。マイクロステートの切り替わりポイントによっては、トラップの処理に費やされた時間が、システム CPU 時間とユーザー CPU 時間を合計したものとして現れることがあります。この時間は、トラップを発行したユーザーのコードの命令またはシステムコールが原因とされます。

一部のシステムコールでは、こうした呼び出しをできるかぎり効率良く処理することが重要とみなされます。こうした呼び出しによって生成されたトラップを高速トラップと呼びます。高速トラップを生成するシステム関数には、gethrtime および gethrvtime があります。 これらの関数ではオーバーヘッドを伴うため、マイクロステートは切り替えられません。

その他にも、トラップをできるかぎり効率良く処理することが重要とみなされる環境があります。たとえば、マイクロステートが切り替えられていないレジスタウィンドウのスピルやフィル、および TLB (translation lookaside buffer) ミスなどです。

いずれの場合も、費やされた時間はユーザー CPU 時間として記録されます。ただし、システムモードに CPU モードが切り替えられたため、ハードウェアカウンタは動作していません。このため、これらのトラップの処理に費やされた時間は、なるべく同じ実験で記録された、ユーザー CPU 時間とサイクル時間の差を取ることで求めることができます。

トラップハンドラがユーザーモードに戻るケースもあります。Fortran で 4 バイトメモリー境界に整列された整数に対し、8 バイトのメモリー参照を行うようなトラップです。スタックにトラップハンドラのフレームが現れ、整数ロードまたはストア命令が原因でパフォーマンスアナライザにハンドラの呼び出しが表示される場合があります。

命令がカーネルにトラップされると、そのトラップ命令のあとの命令の実行に長い時間がかかっているようにみえます。 これは、カーネルがトラップ命令の実行を完了するまで、その命令の実行を開始できないためです。

末尾呼び出しの最適化

特定の関数がその最後で別の関数を呼び出す場合、コンパイラは特別な最適化を行うことができます。新しいフレームを生成するのではなく、呼び出し先が呼び出し元のフレームを再利用し、呼び出し先用の復帰アドレスが呼び出し元からコピーされます。この最適化の目的は、スタックのサイズ削減、および SPARC プラットフォーム上でのレジスタウィンドウの使用削減にあります。

プログラムのソースの呼び出しシーケンスが、次のようになっていると仮定します。

A -> B -> C -> D

B および C に対して末尾呼び出しの最適化を行うと、呼び出しスタックは、関数 A が関数 BCD を直接呼び出しているかのようになります。

A -> B
A -> C
A -> D

つまり、呼び出しツリーがフラットになります。-g オプションを指定してコードをコンパイルした場合、末尾呼び出しの最適化は、4 以上のレベルでのみ行われます。-g オプションなしでコードをコンパイルした場合、2 以上のレベルで末尾呼び出しの最適化が行われます。

明示的なマルチスレッド化

Solaris OS では、簡単なプログラムは、単一の LWP (軽量プロセス) 上のシングルスレッド内で動作します。マルチスレッド化した実行可能ファイルは、スレッド作成関数を呼び出し、その関数に実行するターゲット関数が渡されます。ターゲットの終了時にスレッドが破棄されます。

Solaris OS では、Solaris スレッドと POSIX スレッド (Pthread) の 2 種類のスレッド実装がサポートされています。Solaris 10 OS 以降、両方のスレッド実装が libc.so に含まれます。Solaris 9 OS では、別個のライブラリ libthread.solibpthread.so にスレッド実装が含まれます。

Solaris のスレッドでは、新しく作成されたスレッドは、スレッド作成呼び出しで渡された関数を呼び出す _thread_start() という関数で実行を開始します。このスレッドによって実行されるターゲットが関係するどの呼び出しスタックでも、スタックの先頭は _thread_start() であり、スレッド作成関数の呼び出し元に接続することはありません。このため、作成されたスレッドに関連付けられた包括的メトリックは、_thread_start()<合計> 関数に加算されるだけです。スレッドの作成に加えて、Solaris のスレッド実装では、スレッドを実行するために LWP が Solaris に作成されます。スレッドはそれぞれ特定の LWP に結合されます。

Solaris 10 OS と Linux OS では、明示的なマルチスレッド化に Pthread を使用できます。

どちらの環境でも、新しいスレッドを作成するには、アプリケーションが Pthread API 関数 pthread_create() を呼び出して、関数引数の 1 つとして、ポインタをアプリケーション定義の起動ルーチンに渡します。

Solaris OS では、新しい pthread の実行の開始時に _lwp_start() 関数が呼び出されます。Solaris 10 OS では、_lwp_start() から中間関数 _thr_setup() が呼び出され、その中間関数から pthread_create() で指定されたアプリケーション定義の起動ルーチンが呼び出されます。Solaris 9 OS では、_lwp_start() からアプリケーションの起動ルーチンが直接呼び出されます。

Linux OS では、新しい pthread の実行の開始時に Linux 固有のシステム関数 clone() が呼び出され、この関数から別の内部初期化関数 pthread_start_thread() が呼び出され、この関数から、pthread_create() で定義されたアプリケーション定義の起動ルーチンが呼び出されます。コレクタで使用できる Linux メトリック収集関数はスレッドに固有です。したがって、collect ユーティリティーを実行すると、これは pthread_start_thread() とアプリケーション定義のスレッド起動ルーチンの間に、collector_root() という名前のメトリック収集関数を割り込ませます。

Java テクノロジーベースのソフトウェア実行の概要

典型的な開発者にとっては、Java テクロノジーベースのアプリケーションはほかのプログラムと同じように動作します。このアプリケーションは、一般に class.main というメインエントリポイントから始まり、C または C++ アプリケーションの場合と同様に、ほかのメソッドを呼び出すことがあります。

オペレーティングシステムにとっては、Java プログラミング言語で書かれたアプリケーション (純粋なものか、C/C++ が混合しているもの) は JVM ソフトウェアをインスタンス化するプロセスとして動作します。JVM ソフトウェアは C++ ソースからコンパイルされ、_start から実行を開始し、それが main を呼び出すというように処理が進行します。このソフトウェアは .class ファイルまたは .jar ファイルからバイトコードを読み取り、そのプログラムで指定された操作を実行します。指定できる操作の中には、ネイティブ共有オブジェクトの動的な読み込みや、そのオブジェクト内に含まれている各種関数やメソッドへの呼び出しがあります。

JVM ソフトウェアは、従来の言語で書かれたアプリケーションでは一般に行われない多数のことを行います。起動時に、このソフトウェアはデータ空間に動的に生成されたコードの多数の領域を作成します。これらの領域うちの 1 つは、アプリケーションのバイトコードメソッドを処理するために使用される実際のインタプリタコードです。

Java テクノロジーベースのアプリケーションの実行中、大半のメソッドは JVM ソフトウェアで解析されます。本書では、これらのメソッドをインタプリタされたメソッドと呼んでいます。Java HotSpot 仮想マシンによって、バイトコードの解析時にパフォーマンスが監視され、頻繁に実行されているメソッドが検出されます。繰り返し実行されているメソッドは、Java HotSpot 仮想マシンによってコンパイルされ、マシンコードが生成される場合があります。マシンコードが生成されたメソッドは、コンパイルされたメソッドと呼びます。仮想マシンでは、その後、メソッドの元のバイトコードを解析せずに、より効率的な、コンパイルされたメソッドが実行されます。コンパイルされたメソッドはアプリケーションのデータ空間に読み込まれ、その後のある時点で読み込み解除することができます。さらに、インタプリタされたコードとコンパイルされたコードの間の変換を行うために、ほかのコードがデータ空間で生成されます。

Java プログラミング言語で書かれたコードは、コンパイルされたネイティブコード、すなわち、C、C++、または Fortran 内へ直接呼び出すこともでき、そのような呼び出しのターゲットをネイティブメソッドと呼びます。

Java プログラミング言語で書かれたアプリケーションは本質的にマルチスレッド型であり、ユーザーのプログラム内でスレッドごとに 1 つの JVM ソフトウェアスレッドがあります。Java アプリケーションはまた、シグナル処理、メモリー管理、Java HotSpot 仮想マシンのコンパイルに使用されるハウスキーピングスレッドもいくつかあります。

データの収集は、J2SE 5.0 の JVMTI にあるさまざまなメソッドを使用して実装されます。

Java 呼び出しスタックとマシン呼び出しスタック

パフォーマンスツールは、各 Solaris LWP または Linux スレッドの存続期間中にイベントを記録するほか、イベント時に呼び出しスタックを記録することによってデータを収集します。任意のアプリケーションの実行の任意の時点で、呼び出しスタックは、プログラムが実行のどの段階まで、またどのように達したかを表します。混合モデル Java アプリケーションが従来の C、C++、および Fortran アプリケーションと異なる 1 つの重要な点は、ターゲットの実行中は常に、意味のある呼び出しスタックとして、Java 呼び出しスタックとマシン呼び出しスタックがあるという点です。両方の呼び出しスタックがプロファイル時に記録され、解析時に調整されます。

時間ベースのプロファイルとハードウェアカウンタオーバーフローのプロファイル

Java プログラムに対する時間ベースのプロファイルおよびハードウェアカウンタオーバーフローのプロファイルは、Java 呼び出しスタックとマシン呼び出しスタックの両方が収集されることを除けば、C や C++、Fortran プログラムに対するのと完全に同じ働きをします。

同期トレース

Java プログラムの同期トレースは、スレッドが Java モニターを取得しようとしたときに生成されるイベントに基づいています。これらのイベントに関してはマシン呼び出しスタックと Java 呼び出しスタックの両方が収集されますが、JVM ソフトウェア内で使用される内部ロックに関しては同期トレースデータが収集されません。

ヒープトレース

ヒープトレースデータは、オブジェクト割り当てイベント (ユーザーコードで生成される) とオブジェクト割り当て解除イベント (ガーベージコレクタで生成される) を記録します。また、mallocfree などの C/C++ メモリー管理関数を使用した場合も、記録されるイベントが生成されます。

Java 処理の表現

Java プログラミング言語で書かれたアプリケーションについては、パフォーマンスデータを表示するための表現方法として、Java ユーザー表現、Java 上級表現、マシン表現があります。デフォルトでは、データが Java ユーザー表現をサポートする場合は、Java ユーザー表現で表示されます。以降では、これらの 3 つの表現の主な違いをまとめます。

ユーザー表現

ユーザー表現は、コンパイルされた Java メソッドとインタプリタされた Java メソッドを名前で表示し、ネイティブメソッドをそれらの自然な形式で表示します。実行中は、特定の Java メソッドのインスタンスが、多数存在する場合があります。つまり、インタープリタされたバージョンと、場合によっては 1 つ以上のコンパイルされたバージョンです。Java ユーザー表現では、すべてのメソッドが 1 つのメソッドとして集計された状態で表示されます。アナライザでは、この表現がデフォルトで選択されます。

Java メソッド (Java ユーザー表現内) の PC は、メソッド ID とそのメソッドへのバイトコードのインデックスに対応し、ネイティブ関数の PC はマシン PC に対応します。Java スレッドの呼び出しスタックには Java PC とマシン PC が混ざり合っていることがあります。この呼び出しスタックには、Java ユーザー表現を持たない Java ハウスキーピングコードに対応するフレームはありません。状況によっては、JVM ソフトウェアは Java スタックを展開することができず、特別な関数 <Java 呼び出しスタックが記録されていません> を持つシングルフレームが返されます。これは通常、合計時間の 5 〜 10% にしかなりません。

Java ユーザー表現の関数リストは、Java メソッドと呼び出された任意のネイティブメソッドに対するメトリックを示します。呼び出し元 - 呼び出し先のパネルには、呼び出しの関係が Java ユーザー表現で示されます。

Java メソッドのソースは、コンパイル元の .java ファイル内のソースコードに対応し、各ソース行にメトリックがあります。Java メソッドの逆アセンブリは作成されたバイトコードのほか、各バイトコードに対するメトリックとインタリーブされた Java ソース (入手可能な場合) を示します。

Java ユーザー表現のタイムラインは、Java スレッドのみを示します。各スレッドの呼び出しスタックが、その Java メソッドとともに示されます。

すべての Java プログラムは、通常は monitor-enter ルーチンを呼び出すことで明示的同期化を実行できます。

Java ユーザー表現の同期遅延トレースは、JVMPI 同期イベントをベースとします。通常の同期トレースのデータは、Java ユーザー表現では表示されません。

Java ユーザー表現のデータ空間プロファイリングは、現在サポートされていません。

上級ユーザー表現

Java 上級表現は、JVM 内部要素の詳細のいくつかを除いては Java ユーザー表現に似ています。Java ユーザー表現では表示されない JVM 内部要素の詳細のいくつかが、Java 上級表現に表されます。Java 上級表現では、タイムラインがすべてのスレッドを示し、ハウスキーピングスレッドの呼び出しスタックはネイティブ呼び出しスタックです。

マシン表現

マシン表現には、JVM ソフトウェアでインタプリタされるアプリケーションからの関数でなく、JVM ソフトウェア自体からの関数が表示されます。また、コンパイルされたメソッドとネイティブメソッドがすべて表示されます。マシン表現は、従来の言語で書かれたアプリケーションの表現と同じように見えます。呼び出しスタックは、JVM フレーム、ネイティブフレーム、およびコンパイル済みメソッドフレームを表示します。JVM フレームの中には、インタプリタされた Java、コンパイルされた Java、およびネイティブコードの間の変移コードを表すものがあります。

コンパイルされたメソッドからのソースは Java ソースに対照して表示され、データは選択されたコンパイル済みメソッドの特定のインスタンスを表します。コンパイルされたメソッドの逆アセンブリは、Java バイトコードでなく作成されたマシンアセンブラコードを示します。呼び出し元 - 呼び出し先の関係はすべてのオーバーヘッドフレームと、インタプリタされたメソッド、コンパイルされたメソッド、ネイティブメソッドの間の遷移を表すすべてのフレームを示します。

マシン表現のタイムラインはすべてのスレッド、LWP または CPU のバーを示し、それぞれの呼び出しスタックはマシン表現呼び出しスタックになります。

マシン表現では、スレッド同期が _lwp_mutex_lock への呼び出しに委譲されます。これらの呼び出しはトレースされないので同期データは表示されません。

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

不完全なスタック展開

スタック展開は、次のようないくつかの場合に失敗します。

中間ファイル

-E または -P コンパイラオプションを使用して中間ファイルを生成すると、アナライザはオリジナルのソースファイルではなく、この中間ファイルを注釈付きソースコードとして使用します。-E を使用して生成された #line 指令は、ソース行へのメトリックの割り当てで問題が発生する原因となることがあります。

関数が生成されるようにコンパイルされたソースファイルへの参照用の行番号を持たない関数からの命令が存在する場合、次の行が注釈付きのソースに現れます。

function_name -- <行番号なしの命令>

行番号は、次の条件下では欠落することがあります。