Oracle Solaris Studio 12.2: パフォーマンスアナライザ

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

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

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

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

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

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

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

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

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

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

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

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

3 つの PLT 命令のいずれか 1 つを実行しているときにプロファイリングイベントが発生した場合、PLT PC は削除され、排他的時間はその呼び出し命令に対応することになります。PLT エントリによる最初の呼び出し時にプロファイリングイベントが発生し、かつリーフ PC が PLT 命令のいずれかではない場合、PLT および ld.so のコードから発生する 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 look aside buffer、変換索引バッファー) ミスやレジスタウィンドウのスピル/フィルの場合も、マイクロステートは切り替わりません。

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

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

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

末尾呼び出しの最適化

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

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

A -> B -> C -> D

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

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

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

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

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

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

Solaris のスレッドでは、新しく作成されたスレッドは、スレッド作成呼び出しで渡された関数を呼び出す _thread_start() という関数で実行を開始します。このスレッドによって実行されるターゲットが関係するどの呼び出しスタックでも、スタックの先頭は _thread_start() であり、スレッド作成関数の呼び出し元に接続することはありません。このため、作成されたスレッドに関連付けられた包括的メトリックスは、_thread_start()<Total> 関数にのみ及びます。スレッドの作成に加えて、Solaris のスレッド実装では、スレッドを実行するために LWP が Solaris 上に作成されます。LWPSolaris スレッドによる作成スレッドはそれぞれ特定の 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() で指定されたアプリケーション定義の起動ルーチンが呼び出されます。

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 の JVMTI にあるさまざまなメソッドを使用して実装されます。

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

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

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

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

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 スタックを展開することができず、<no Java callstack recorded> という特別な関数を持つシングルフレームが返されます。これは通常、合計時間の 5 ~ 10% にしかなりません。

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

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

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

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

上級ユーザー表現

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

マシン表現

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

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

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

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

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

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

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

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

bardo_ -- OMP parallel region from line 9 [_$p1C9.bardo_]
atomsum_ -- MP doall from line 7 [_$d1A7.atomsum_]

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

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

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

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

OpenMP プログラムのデータ収集についての詳細は、OpenMP のユーザーコミュニティー Web サイトの An OpenMP Runtime API for Profiling を参照してください。

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

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

ユーザーモードでは、プロファイルデータの表示はモデルにさらに近くなるよう変更されるため、記録されたデータやマシンモードの表示と次の 3 つの点で異なっています。

擬似関数

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

次の疑似関数が定義されています。

<OMP-overhead>

OpenMP ライブラリ内で実行中 

<OMP-idle>

作業を待っているスレーブスレッド 

<OMP-reduction>

縮約操作を実行中のスレッド  

<OMP-implicit_barrier>

暗黙のバリアで待機中のスレッド 

<OMP-explicit_barrier>

明示的なバリアで待機中のスレッド 

<OMP-lock_wait>

ロックを待っているスレッド 

<OMP-critical_section_wait>

critical セクションに入るのを待っているスレッド 

<OMP-ordered_section_wait>

ordered セクションに入る順番を待っているスレッド 

<OMP-atomic_section_wait>

OpenMP 原子構造で待機中のスレッド 

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

OpenMP 3.0 プログラムでは、<OMP-overhead> 擬似関数は使用されません。この擬似関数は、OpenMP オーバーヘッドメトリックスに置き換えられています。

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

OpenMP 実験の場合は、OpenMP を使用せずにプログラムがコンパイルされたときに取得されたものと同様の、再構築された呼び出しスタックが表示されます。目標は、実際の処理の詳細すべてを示すことではなく、プログラムを直観的に理解できるようにプロファイルデータを表示することです。OpenMP ランタイムライブラリが特定の操作を実行しているときは、マスタスレッドとスレーブスレッドの呼び出しスタックがまとめられ、呼び出しスタックに <OMP-*> 擬似関数が追加されます。

OpenMP メトリックス

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

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

OpenMP 待ちメトリックスとOpenMP ワークメトリックスは、ユーザーモード、上級モード、およびマシンモードに表示されます。

OpenMP プロファイリングデータの上級表示モード

上級表示モードで OpenMP 実験を確認する場合、OpenMP ランタイムが特定の操作を実行しているときの <OMP-*> 形式の擬似関数が、ユーザー表示モードと同じように表示されます。ただし、上級表示モードでは、並列化されたループ、タスクなどを表す、コンパイラにより生成された mfunction が別個に表示されます。ユーザーモードでは、コンパイラにより生成されたこれらの mfunction はユーザー関数に含められます。

OpenMP プロファイリングデータのマシン表示モード

「マシン」モードでは、すべてのスレッドのネイティブな呼び出しスタックと、コンパイラによって生成されたアウトライン関数が表示されます。

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

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

  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 として表示されます。foo-OMP... の呼び出しは、並列領域に対して生成された mfunction を表します。

  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 -- <instructions without line numbers>

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