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

第 7 章 パフォーマンスアナライザとそのデータの内容

パフォーマンスアナライザは、コレクタが収集したイベントデータを読み取り、そのデータをパフォーマンスメトリックに変換します。 メトリックは、ターゲットプログラムの構造内の、命令、ソース行、関数、ロードオブジェクトなどのさまざまな要素について計算されます。収集されたあらゆるイベントについて、ヘッダーと、次の 2 つの部分からなるデータが記録されます。

プログラム構造にメトリックを関連付ける処理は、常に簡単にできるとはかぎりません。 これは、コンパイラによって、コードの挿入や変換、最適化が行われるためです。この章では、この処理を説明するとともに、パフォーマンスアナライザの表示にそのことがどのように反映されるのかという問題を取り上げます。

この章では、次の内容について説明します。

データ収集の作用

データ収集の実行による出力は実験であり、さまざまな内部ファイルとサブディレクトリを持つディレクトリとしてファイルシステム内に格納されます。

実験の形式

すべての実験には、次の 3 つのファイルが含まれています。

また、実験にはプロセスが存続している間のプロファイルイベントを表すバイナリデータファイルがあります。各データファイルには、「パフォーマンスメトリックの解釈」で説明しているように、一連のイベントがあります。データの種類ごとに個別のファイルを使用しますが、各ファイルはターゲット内のすべての LWP で共有されます。データファイルは、次のような名前が付いています。

表 7–1 データの種類と対応するファイル名

データの種類 

ファイル名 

時間ベースのプロファイル 

profile

ハードウェアカウンタオーバーフローのプロファイル 

hwcounters

同期トレース 

synctrace

ヒープトレース 

heaptrace

MPI トレース 

mpitrace

OpenMP トレース 

omptrace

時間ベースのプロファイルまたはハードウェアカウンタオーバーフローのプロファイルの場合、データはクロック刻みまたはカウンターオーバーフローによって呼び出されたシグナルハンドラに書き込まれます。同期トレース、ヒープトレース、MPI トレース、または OpenMP トレースの場合は、通常のユーザー呼び出しルーチンで LD_PRELOAD 環境変数により割り込み処理される libcollector.so ルーチンからデータが書き込まれます。そのような割り込み処理ルーチンは部分的にデータレコードを記入したあと、通常のユーザー呼び出しルーチンを呼び出し、ルーチンが復帰したときにデータレコードの残りの部分を記入し、データファイルにレコードを書き込みます。

すべてのデータファイルはメモリーマップされ、ブロック単位で書き込まれます。レコードは常に有効なレコード構造を持つように記入されるので、実験は書き込み中に読み取ることができます。LWP 間の競合とシリアル化を最小限にするために、バッファー管理戦略が設計されています。

オプションで、notes というファイル名の ASCII ファイルを実験に含めることができます。このファイルは、collect コマンドに -C comment 引数を使用すると、自動的に作成されます。実験の作成後、ファイルを手動で編集または作成できます。ファイルの内容は、実験のヘッダーの先頭に付加されます。

archives ディレクトリ

各実験には archives ディレクトリがあり、このディレクトリには、map.xml ファイル内で参照されている各ロードオブジェクトについて記述したバイナリファイルがあります。これらのファイルは、データ収集の終了時に実行される er_archive ユーティリティーによって作成されます。プロセスが異常終了すると、er_archive ユーティリティーが呼び出されない場合があります。その場合、アーカイブファイルは最初に実験に対して er_print ユーティリティーまたはアナライザを呼び出したときに書き込まれます。

派生プロセス

派生プロセスは、その実験データを親プロセスの実験内のサブディレクトリに書き込みます。

これらの新しい実験には、次のようにそれぞれの系統を示す名前が付けられます。

たとえば親プロセスの実験名が test.1.er の場合、3 回目の fork の呼び出しで作成された子プロセスの実験は test.1.er/_f3.er となります。この子プロセスが新しいイメージを実行した場合、対応する実験名は test.1.er/_f3_x1.er となります。派生実験は親の実験と同じファイルから構成されていますが、派生実験を持たず (すべての派生は親の実験内のサブディレクトリで表される)、アーカイブサブディレクトリを持っていません (すべてのアーカイブが親の実験内へ行われる)。

動的な関数

ターゲットが動的な関数を作成する実験には、map.xml ファイル内に動的な関数を記述する追加レコードと、動的な関数の実際の命令のコピーを含む追加ファイル dyntext があります。動的な関数の注釈付き逆アセンブリを生成するには、このコピーが必要です。

Java 実験

Java 実験の map.xml ファイル内には、その内部処理用に JVM ソフトウェアで作成された動的な関数用と、ターゲット Java メソッドの動的にコンパイルされた (HotSpot) バージョン用の追加レコードがあります。

また、Java 実験には、呼び出された、ユーザーのすべての Java クラスに関する情報を含む JAVA_CLASSES ファイルがあります。

Java トレースデータは、libcollector.so の一部である JVMTI エージェントを使用して記録されます。エージェントは、記録されたトレースイベントへマップされるイベントを受け取ります。このエージェントは、JAVA_CLASSES ファイルの書き込みに使用するクラスの読み込みと HotSpot のコンパイルのためのイベント、および map.xml ファイル内の Java でコンパイルされたメソッドレコードも受信します。

実験の記録

実験を記録する方法には、次の 3 つがあります。

アナライザ GUI のパフォーマンスツールの「収集」ウィンドウは collect による実験を実行し、IDE の「コレクタ」ダイアログボックスは dbx による実験を実行します。

collect による実験

collect コマンドを使用して実験を記録する場合、collect ユーティリティーは実験ディレクトリを作成し、libcollector.so がターゲットのアドレス空間にあらかじめ読み込まれるようにLD_PRELOAD 環境変数を設定します。その後、libcollector.so に実験名を知らせるための環境変数とデータ収集オプションを設定し、ターゲットをその上で実行します。

libcollector.so は、すべての実験ファイルの書き込みを行います。

dbx でプロセスを生成する実験

データ収集を有効にした状態で dbx を使用してプロセスを起動すると、dbx は実験ディレクトリも作成し、libcollector.so が事前に読み込まれるようにします。dbx は最初の命令の前のブレークポイントでプロセスを停止し、次にデータ収集を開始するために libcollector.so 内の初期化ルーチンを呼び出します。

Java 実験データが dbx で収集できないのは、 dbx がデバッグのために Java Virtual Machine Debug Interface (JVMDI) エージェントを使用し、そのエージェントがデータの収集に必要な Java Virtual Machine Tools Interface (JVMTI) エージェントと共存できないからです。

dbx による実行中のプロセスの実験

dbx を使用して実行中のプロセスで実験を開始すると、dbx は実験ディレクトリを作成しますが、LD_PRELOAD 環境変数を使用できません。dbx は対話式関数呼び出しをターゲット内に行なって libcollector.so を開き、次にプロセスを作成する場合と同様に libcollector.so の初期化ルーチンを呼び出します。データは、collect 実験の場合と同様に libcollector.so によって書き込まれます。

プロセスが開始したときに libcollector.so はターゲットアドレス空間になかったので、ユーザー呼び出し可能関数 (同期トレース、ヒープトレース、MPI トレース) に対する割り込み処理に依存するデータ収集は機能しない場合があります。一般に、シンボルはすでに基礎的な関数に分解されているので、割り込み処理は行えません。さらに、派生プロセスの次も割り込み処理に依存し、実行中プロセスで dbx により作成された実験に対して適切に機能しません。

dbx でプロセスを開始する前、または dbx で実行中プロセスに接続する前に明示的に libcollector.so を事前読み込みした場合は、トレースデータを収集できます。

パフォーマンスメトリックの解釈

各イベントのデータには、高分解能のタイムスタンプ、スレッド ID、LWP ID、プロセッサ ID が含まれます。これらの最初の 3 つのデータを使用すれば、時間、スレッド、または LWP によってパフォーマンスアナライザでメトリックのフィルタ処理が行えます。プロセッサ ID については、getcpuid(2) のマニュアルページを参照してください。getcpuid を利用できないシステムでのプロセッサ ID は -1 であり、Unknown にマップされます。

各イベントでは、共通データ以外に、以降の節で説明する固有の raw データが生成されます。これらの節ではまた、raw データから得られるメトリックの精度と、データ収集がメトリックに及ぼす影響についても説明しています。

時間ベースのプロファイリング

時間ベースのプロファイリングのイベント固有のデータは、プロファイル間隔カウント値からなる配列で構成されています。Solaris OS の場合は、間隔カウンタが提供されます。プロファイル間隔の最後で適切な間隔カウンタが 1 増分され、別のプロファイル信号がスケジューリングされます。この配列が記録され、リセットされるのは、Solaris LWP スレッドが CPU ユーザーモードに入った場合だけです。配列のリセット時には、ユーザー CPU 状態の配列要素が 1 に設定され、ほかの全状態の配列要素が 0 に設定されます。配列データが記録されるのは、配列がリセットされる前にユーザーモードに入るときです。したがって、配列には、Solaris LWP ごとにカーネルが保持する 10 個のマイクロステートのそれぞれについて、ユーザーモードに前回入って以降の各マイクロステートのカウントの累計値が含まれます。Linux OS ではマイクロステートは存在せず、利用できる間隔カウンタはユーザー CPU 時間だけです。

呼び出しスタックは、データと同時に記録されます。プロファイル間隔の最後で Solaris LWP がユーザーモードでない場合は、LWP またはスレッドが再びユーザーモードになるまで、呼び出しスタックの内容は変わりません。すなわち、呼び出しスタックには、各プロファイル間隔の最後のプログラムカウンタの位置が常に正確に記録されます。

表 7–2 に、各マイクロステートとメトリックの、Solaris OS における対応関係をまとめます。

表 7–2 カーネルのマイクロステートとメトリックの対応関係

カーネルのマイクロステート 

内容の説明 

メトリック名 

LMS_USER

ユーザーモードで動作 

ユーザー CPU 時間 

LMS_SYSTEM

システムコールまたはページフォルトで動作 

システム CPU 時間 

LMS_TRAP

上記以外のトラップで動作 

システム CPU 時間 

LMS_TFAULT

ユーザーテキストページフォルトでスリープ 

テキストページフォルト時間 

LMS_DFAULT

ユーザーデータページフォルトでスリープ 

データページフォルト時間 

LMS_KFAULT

カーネルページフォルトでスリープ 

ほかの待ち時間 

LMS_USER_LOCK

ユーザーモードロック待ちのスリープ 

ユーザーロック時間 

LMS_SLEEP

ほかの理由によるスリープ 

ほかの待ち時間 

LMS_STOPPED

停止 (/proc、ジョブ制御、lwp_stop のいずれか)

ほかの待ち時間 

LMS_WAIT_CPU

CPU 待ち 

CPU 待ち時間 

タイミングメトリックの精度

タイミングデータは統計データとして収集されます。 このため、ほかの統計的な標本収集手法と同様に、あらゆる誤差の影響を受けます。プログラムの実行時間が非常に短い場合は、少数のプロファイルパケットしか記録されず、多くのリソースを消費するプログラム部分が、呼び出しスタックに反映されないことがあります。このため、目的の関数またはソース行について数百のプロファイルパケットを蓄積するのに十分な時間または十分な回数に渡って、プログラムを実行するようにしてください。

統計的な標本収集の誤差のほかに、データの収集・関連付け方法、システムにおけるプログラムの実行の進み具合を原因とする誤差もあります。次に示す環境などでは、タイミングメトリックでデータに不正確さやひずみが生じる可能性があります。

これらの不正確さのほかにも、データ収集処理そのものが原因でタイミングメトリックが不正確になります。記録はプロファイルシグナルによって開始されるため、プロファイルパケットの記録に費やされた時間が、プログラムのメトリックに反映されることはありません。これは、相関関係の別の例です。記録に費やされたユーザー CPU 時間は、記録されるあらゆるマイクロステート値に配分されます。この結果、ユーザー CPU 時間のメトリックが実際より小さくなり、その他のメトリックが実際より大きくなります。デフォルトのプロファイル間隔の場合、一般に、データの記録に費やされる時間は CPU 時間の 2、3% 未満です。

タイミングメトリックの比較

時間ベースの実験のプロファイリングで得られたタイミングメトリックと、その他の方法で得られた時間を比較する場合は、次の点に注意する必要があります。

シングルスレッドアプリケーションの場合、通常 1 つのプロセスについて記録された Solaris LWP または Linux スレッド合計時間は、同じプロセスについて gethrtime(3C) によって返される値と比較すると、数十分の 1 パーセントの精度になります。CPU 時間の場合は、同じプロセスについて gethrvtime(3C) によって返される値と比較して、数パーセント程度異なることがあります。負荷が大きい場合は、差がさらに大きくなることがあります。ただし、CPU 時間の差は規則的なひずみを表すものではなく、関数、ソース行などについて報告される相対時間に大きなひずみはありません。

Solaris OS の非結合スレッドを使用するマルチスレッドアプリケーションの場合、gethrvtime() によって返される値の差が無意味であることがあります。これは、gethrvtime() は LWP について値を返し、スレッドは LWP ごとに異なることがあるからです。

パフォーマンスアナライザの報告する LWP 時間が、vmstat の報告する時間とかなり異なることがあります。 これは、vmstat が CPU 全体にまたがって集計した時間を報告するためです。たとえば、ターゲットプロセスの LWP 数が、そのプロセスが動作するシステムの CPU 数よりも多い場合、アナライザは、vmstat が報告する時間よりもずっと長い待ち時間を報告します。

パフォーマンスアナライザの「統計」タブと er_print 統計ディスプレイに表示されるマイクロステート時間値は、プロセスファイルシステムの /proc 使用報告に基づいており、この報告には、マイクロステートで費やされる時間が高い精度で記録されます。詳細は、proc (4) のマニュアルページを参照してください。これらのタイミング値と <合計> 関数 (プログラム全体を表す) のメトリックを比較することによって、集計されたタイミングメトリックのおおよその精度を知ることができます。ただし、「統計」タブに表示される値には、<合計> の時間メトリック値に含まれないその他の寄与要素が含まれることがあります。その原因は、データ収集が一時停止される期間によるものです。

ユーザー CPU 時間とハードウェアカウンタサイクル時間は異なります。なぜなら、ハードウェアカウンタは、CPU モードがシステムモードへ切り替えられたときにオフにされるからです。詳細は、「トラップ」を参照してください。

同期待ちトレースデータ

同期待ちトレースは、Solaris プラットフォームでのみ利用できます。コレクタは、スレッドライブラリ libthread.so 内の関数の呼び出しまたは リアルタイム拡張ライブラリ librt.so の呼び出しをトレースすることによって、同期遅延イベントのデータを収集します。イベント固有のデータは、要求と許可 (トレース対象の呼び出しの始まりと終わり) の高分解能のタイムスタンプと同期オブジェクト (要求された相互排他ロックなど) のアドレスで構成されます。スレッド ID と LWP ID は、データが記録された時点での ID です。 要求時刻と許可時刻の差が待ち時間です。記録されるイベントは、指定したしきい値を要求と許可の時間差が超えたものだけです。同期待ちトレースデータは、許可時に実験ファイルに記録されます。

遅延の原因となったイベントが完了しないかぎり、待ちスレッドがスケジューリングされている LWP がほかの作業を行うことはできません。この待ち時間は、「同期待ち時間」と「ユーザーロック時間」の両方に反映されます。同期遅延しきい値は短時間の遅延を排除するので、「ユーザーロック時間」が「同期待ち時間」よりも大きくなる可能性があります。

待ち時間は、データ収集のオーバーヘッドによってひずみます。そして、このオーバーヘッドは、収集されたイベントの個数に比例します。オーバーヘッドに費やされた待ち時間の一部は、イベント記録しきい値を大きくすることによって最小にできます。

ハードウェアカウンタオーバーフローのプロファイリング

ハードウェアカウンタオーバーフローのプロファイルデータには、カウンタ ID とオーバーフロー値が含まれます。この値は、カウンタがオーバーフローするように設定されている値よりも大きくなることがあります。 これは、オーバーフローが発生して、そのイベントが記録されるまでの間に命令が実行されるためです。この値は特に、浮動小数点演算やキャッシュミスなどのカウンタよりも、ずっと頻繁に増分されるサイクルカウンタや命令カウンタの場合に大きくなる可能性があります。イベント記録時の遅延はまた、呼び出しスタックとともに記録されたプログラムカウンタのアドレスが正確にオーバーフローイベントに対応しないことを意味します。詳細は、「ハードウェアカウンタオーバーフローの関連付け」を参照してください。また、「トラップ」も参照してください。トラップおよびトラップハンドラは、ユーザーの CPU 時間とサイクルカウンタによって報告される時間の間の、大きな相違の原因になることがあります。

収集されるデータ量は、オーバーフロー値に依存します。選択した値が小さすぎると、次のような影響が出ることがあります。

値が大きすぎると、良好な統計情報を得るにはオーバーフローの発生が少なくすぎる可能性があります。最後のオーバーフローの発生後に生じたカウントは、コレクタ関数の collector_final_counters によるものです。この関数がカウント値のかなりの割合を占める場合は、オーバーフロー値が大きすぎます。

ヒープトレース

コレクタは、メモリーの割り当てと割り当て解除の関数である mallocreallocmemalignfree に割り込むことによって、これらの関数の呼び出しに関するトレースデータを記録します。メモリーを割り当てるときにこれらの関数を迂回するプログラムの場合、トレースデータは記録されません。別のメカニズムが使用されている Java メモリー管理では、トレースデータは記録されません。

トレース対象の関数は、さまざまなライブラリから読み込まれる可能性があります。パフォーマンスアナライザで表示されるデータは、読み込み対象の関数が属しているライブラリに依存することがあります。

短時間で大量のトレース対象関数を呼び出すプログラムの場合、プログラムの実行に要する時間が大幅に長くなることがあります。延びた時間は、トレースデータの記録に使用されます。

データ空間プロファイリング

データ空間プロファイルはキャッシュミスなどのメモリー関係のイベントの報告データを集めたもので、メモリー関係のイベントが発生する命令だけではなく、イベントを発生させるデータオブジェクト参照についても報告します。データ空間プロファイリングは、Linux システムでは利用できません。

データ空間プロファイリングを可能にするには、ターゲットは、-xhwcprof フラグと -xdebugformat=dwarf -g フラグを付けて SPARC アーキテクチャー用にコンパイルされた C プログラムである必要があります。さらに、収集されるデータは、メモリー関係のカウンタのハードウェアカウンタプロファイルでなければならず、カウンタ名の前にオプションの + 記号を付加する必要があります。パフォーマンスアナライザには、データ空間プロファイリング関係のタブとして、「データオブジェクト」タブと「データレイアウト」タブのほか、メモリーオブジェクト用の各種のタブが含まれています。

データ空間プロファイリングは、プロファイル間隔の前にプラス記号 (+) を付加することで、時間ベースのプロファイリングとともに実施できます。

MPI トレース

MPI トレースは、Solaris プラットフォームでのみ利用できます。MPI トレースは、MPI ライブラリ関数の呼び出しに関する情報を記録します。イベント固有のデータは、要求と許可 (トレース対象の呼び出しの始まりと終わり) の高分解能のタイムスタンプ、および送受信動作の数と送受信バイト数で構成されます。トレースは、MPI ライブラリの呼び出しに割り込むことによって行われます。割り込み関数は、データ送信の最適化に関する情報や送信エラーに関する情報を持たないので、提示される情報は、以降で説明する単純な形でのデータ送信を表しています。

受信バイト数は、MPI 関数の呼び出しで定義されるバッファーサイズです。実際に受信したバイト数は、割り込み関数には利用できません。

一部の「大域通信」関数は、ルートと呼ばれる単一の受信プロセスまたは単一の起点を持ちます。こういった関数のアカウンティングは、次のように行われます。

次の例は、アカウンティングの手順を示しています。これらの例における G は、グループのサイズです。

MPI_Bcast() の呼び出しの場合、

MPI_Allreduce() の呼び出しの場合、

MPI_Reduce_scatter() の呼び出しの場合、

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

呼び出すスタックは、プログラム内の命令を示す一連のプログラムカウンタ (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 -- <行番号なしの命令>

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

プログラム構造へのアドレスのマッピング

アナライザは、呼び出しスタックの内容を処理して PC 値を生成したあとに、それらの PC をプログラム内の共有オブジェクト、関数、ソース行、逆アセンブリ行 (命令) にマップします。ここでは、これらのマッピングについて説明します。

プロセスイメージ

プログラムを実行すると、そのプログラムの実行可能ファイルからプロセスがインスタンス化されます。プロセスのアドレス空間には、実行可能な命令を表すテキストが存在する領域や、通常は実行されないデータが存在する領域などの多数の領域があります。通常、呼び出しスタックに記録される PC は、プログラムのいずれかのテキストセグメント内のアドレスに対応しています。

プロセスの先頭テキストセクションは、実行可能ファイルそのものから生成されます。先頭以外のテキストセクションは、プロセスの開始時に実行可能ファイルとともに読み込まれたか、プロセスによって動的に読み込まれた、共有オブジェクトに対応しています。呼び出しスタック内の PC は、呼び出しスタックの記録時に読み込まれた実行可能ファイルと共有オブジェクトに基づいて解決されます。実行可能ファイルと共有オブジェクトはよく似ているため、これらをまとめてロードオブジェクトと呼びます。

共有オブジェクトは、プログラムの実行途中で読み込みおよび読み込み解除できるため、実行中のタイミングによって PC が対応する関数が異なることがあります。また、共有オブジェクトが読み込み解除されたあとに別のアドレスに再度読み込まれた場合は、異なる時点で異なる複数の PC が同じ関数に対応することもあります。

ロードオブジェクトと関数

実行可能ファイルまたは共有オブジェクトのどちらであっても、ロードオブジェクトには、コンパイラによって生成された命令を含むテキストセクション、データ用のデータセクション、さまざまなシンボルテーブルが含まれます。すべてのロードオブジェクトには、ELF シンボルテーブルが存在する必要があります。ELF シンボルテーブルには、そのオブジェクト内で大域的に既知の関数すべての名前とアドレスが含まれます。-g オプションを指定してコンパイルしたロードオブジェクトには、追加のシンボル情報が含まれます。 この情報は、ELF シンボルテーブルを補足するもので、非大域的な関数に関する情報、関数の派生元のオブジェクトモジュールに関する補足情報、アドレスをソース行に関連付ける行番号情報で構成されます。

関数」という用語は、ソースコードで記述された高度な演算を表す一群の命令を表します。この用語は、Fortran で使用されるサブルーチン、C++ および Java プログラミング言語などで使用されているメソッドなども表します。サブルーチン関数はソースコードで明確に記述され、通常、その名前は、一群のアドレスを表すシンボルテーブル内に出現します。 プログラムカウンタ値がアドレスセットに含まれているということは、プログラムの実行がその関数で起こっていることを意味します。

基本的に、ロードオブジェクトのテキストセグメント内のアドレスは、関数にマップすることができます。呼び出しスタック上のリーフ PC およびほかのすべての PC で、まったく同じマッピング情報が使用されます。関数の多くは、プログラムのソースモデルに直接対応します。以降の節では、そのような対応関係を持たない関数について説明します。

別名を持つ関数

通常、関数は大域関数と定義されます。 このことは、プログラム内のあらゆる部分で関数名が既知であることを意味します。大域関数の名前は、実行可能ファイル内で一意である必要があります。アドレス空間内に同一名の大域関数が複数存在する場合、実行時リンカーはすべての参照をそのうちの 1 つに決定します。その他の関数は実行されず、関数リストにそれらの関数が含まれることはありません。「概要」タブでは、選択した関数を含む共有オブジェクトおよびオブジェクトモジュールを調べることができます。

さまざまな状況で、同じ関数が異なる名前で認識されることがあります。この一般的な例としては、コードの同一部分に対して、いわゆる弱いシンボルと強いシンボルが使用されている場合があります。一般に、強い名前は対応する弱い名前と同じですが、前に下線 (_) が付きます。スレッドライブラリ内の多くの関数には、強い名前、弱い名前、代替内部シンボルに加えて、pthread および Solaris スレッド用に別の名前があります。いずれの場合も、アナライザの関数リストでは、このうちの 1 つの名前だけが使用されます。選択される名前は、アドレスをアルファベット順に並べた最後のシンボルです。ほとんどの場合は、この名前がユーザーの使用する名前に対応しています。「概要」タブでは、選択した関数のすべてのエイリアス (別名) が表示されます。

一意でない関数名

別名を持つ関数は、コードの同一部分に複数の名前があることを意味しますが、場合によっては、複数のコード部分に同一名が使用されていることがあります。

ストリップ済み共有ライブラリの静的関数

ライブラリ内では静的関数がよく使用されます。これは、ライブラリ内部の関数名がユーザーの使う関数名と衝突しないようにするためです。ライブラリをストリップすると、静的関数の名前はシンボルテーブルから削除されます。このような場合、アナライザは、ライブラリ内でストリップ済み静的関数を含むテキスト領域ごとに擬似的な名前を生成します。この名前は <static>@0x12345 という形式で、@ 記号に続く文字列は、ライブラリ内のテキスト領域のオフセット位置を表します。アナライザは、連続する複数のストリップ済み静的関数と単一のストリップ済み静的関数を区別できないため、複数のストリップ済み静的関数のメトリックがまとめて表示されることがあります。

ストリップ済み静的関数は、その PC が静的関数の保存命令のあとに表示されるリーフ PC である場合を除いて、正しい呼び出し元から呼び出されたように表示されます。シンボル情報がない場合、アナライザは保存アドレスを認識しません。 このため、復帰レジスタを呼び出し元として使用すべきかどうかは判断できません。復帰レジスタは常に無視されます。複数の関数が、1 つの <static>@0x12345 関数にまとめられることがあるため、実際の呼び出し元または呼び出し先が隣接する関数と区別されないことがあります。

Fortran の代替エントリポイント

Fortran には、コードの一部に複数のエントリポイントを用意し、呼び出し元が関数の途中を呼び出す手段が用意されています。このようなコードをコンパイルしたときに生成されるコードは、メインのエントリポイントの導入部、代替エントリポイントの導入部、関数のコード本体で構成されます。各導入部では、関数の最終的な復帰用のスタックが作成され、そのあとで、コード本体に分岐または接続します。

各エントリポイントの導入部のコードは、そのエントリポイント名を持つテキスト領域に常に対応しますが、サブルーチン本体のコードは、エントリポイント名の 1 つだけを受け取ります。受け取る名前は、コンパイラによって異なります。

多くの場合、導入部の時間はわずかで、アナライザに、サブルーチン本体に関連付けられたエントリポイント以外のエントリポイントに対応する関数が表示されることはほとんどありません。通常、代替エントリポイントを持つ Fortran サブルーチンで費やされる時間を表す呼び出しスタックは、導入部ではなくサブルーチンの本体に PC があり、本体に関連付けられた名前だけが呼び出し先として表示されます。同様に、そうしたサブルーチンからのあらゆる呼び出しは、サブルーチン本体に関連付けられている名前から行われたものとみなされます。

クローン生成関数

コンパイラは、通常以上の最適化が可能な関数への呼び出しを見分けることができます。こういった呼び出しの一例としては、引数の一部が定数である関数への呼び出しが挙げられます。最適化できる呼び出しを見つけると、コンパイラは、この関数のコピー (クローンと呼ばれる) を作成し、最適化コードを生成します。クローン関数名は、特定の呼び出しを識別する、符号化された名前です。アナライザはこの名前を復号化し、クローン生成関数のインスタンスそれぞれを別々に関数リストに表示します。クローン生成関数はそれぞれ別の命令セットを持っているので、注釈付き逆アセンブリリストには、クローン生成関数が別々に表示されます。各クローン生成関数のソースコードは同じであるため、注釈付きソースリストでは関数のあらゆるコピーについてデータが集計されます。

インライン化された関数

インライン化された関数は、コンパイルすると実際の呼び出しの代わりに関数の呼び出し位置に命令が挿入されます。2 通りのインライン化があり、ともにパフォーマンス向上のために行われ、アナライザに影響します。

いずれのインライン化も、メトリックの表示に同じ影響を及ぼします。ソースコードに記述されていて、インライン化された関数は、関数リストにも、また、そうした関数のインライン化先の関数の呼び出し先としても表示されません。通常ならば、インライン化された関数の呼び出し位置で包括的メトリックとみなされるメトリック (呼び出された関数で費やされた時間を表す) が、実際には呼び出し位置 (インライン化された関数の命令を表す) が原因の排他的メトリックと報告されます。


注 –

インライン化によってデータの解釈が難しくなることがあります。 このため、パフォーマンス解析のためにプログラムをコンパイルするときには、インライン化を無効にすることを推奨します。


場合によっては、関数がインライン化されている場合も、いわゆるライン外関数が残されます。一部の呼び出し側ではライン外関数が呼び出され、それ以外では命令がインライン化されます。このような場合、関数は関数リストに含まれますが、関連するメトリックはライン外呼び出しだけを表します。

コンパイラ生成の本体関数

関数内のループまたは並列化指令のある領域を並列化する場合、コンパイラは、元のソースコードに含まれていない新しい本体関数を作成します。こうした関数については、「OpenMP ソフトウェアの実行の概要」で詳しく説明しています。

アナライザは、このような本体関数を通常の関数として表示し、コンパイラ生成名に加え、その関数の抽出元の関数に基づいてその関数に名前を割り当てます。こうした関数の排他的メトリックおよび包括的メトリックは、本体関数で費やされた時間を表します。また、構造の抽出元の関数は各本体関数の包括的メトリックを示します。これが達成される方法については、「OpenMP ソフトウェアの実行の概要」で説明しています。

並列ループを含む関数をインライン化した場合、そのコンパイラ生成の本体関数名には、元の関数ではなく、インライン化先の関数の名前が反映されます。


注 –

コンパイラ生成本体関数の名前は、-g オプションを指定してコンパイルされたモジュールでのみ復号化することができます。


アウトライン関数

フィードバック最適化コンパイルで、アウトライン関数が作成されることがあります。 それらは、通常では実行されないコード、特に、最終的な最適化コンパイル用のフィードバックの生成に使用される「試験実行」の際に実行されないコードを表しています。一般的な例は、ライブラリ関数の戻り値でエラーチェックを実行するコードです。通常、エラー処理コードは実行されません。ページングと命令キャッシュの動作を向上させるため、こういったコードはアドレス空間の別の場所に移動され、新たな別の関数となります。アウトライン関数の名前は、コードの取り出し元関数の名前や特定のソースコードセクションの先頭の行番号を含む、アウトライン化したコードのセクションに関する情報をエンコードします。これらの符号化された名前は、リリースごとに異なります。アナライザは、読みやすい関数名を提供します。

アウトライン関数は実際には呼び出されるのではなく、ジャンプ先になります。同様に、アウトライン関数は復帰するのではなく、ジャンプ先から戻ります。動作をユーザーのソースコードモデルにより近づけるため、アナライザは、main 関数からそのアウトライン部分への擬似的な呼び出しを生成します。

アウトライン関数は、適切な包括的および排他的メトリックを持つ通常の関数として表示されます。また、アウトライン関数のメトリックは、アウトライン化元の関数の包括的メトリックとして追加されます。

フィードバックデータを利用した最適化コンパイルの詳細は、『 C ユーザーズガイド』の付録 B、『C++ ユーザーズガイド』の付録 A、または『Fortran ユーザーズガイド』の第 3 章で、-xprofile コンパイラオプションの説明を参照してください。

動的にコンパイルされる関数

動的にコンパイルされる関数は、プログラムの実行中にコンパイルされてリンクされる関数です。コレクタ API 関数を使用して必要な情報をユーザーが提供しないかぎり、コレクタは C や C++ で記述された動的にコンパイルされる関数に関する情報を把握できません。API 関数については、「動的な関数とモジュール」を参照してください。情報を提供しなかった場合、関数は <未知> としてパフォーマンス解析ツールに表示されます。

Java プログラムの場合、コレクタは Java HotSpot 仮想マシンによってコンパイルされるメソッドに関する情報を取得するので、API 関数を使用して情報を提供する必要がありません。ほかのメソッドの場合、パフォーマンスツールはメソッドを実行する JVM ソフトウェアの情報を表示します。Java ユーザー表現では、すべてのメソッドがインタプリタされたバージョンとマージされます。マシン表現では、HotSpot でコンパイルされたバージョンが個別に表示され、JVM 関数はインタプリタされたメソッドごとに表示されます。

<未知> 関数

状況によっては、PC が既知の関数にマップされないことがあります。このような場合、PC は <未知> という名前の特別な関数にマップされます。

PC が <未知> にマップされるのは、次のような場合です。

<未知> 関数の呼び出し元および呼び出し先は、呼び出しスタックの前および次の PC に対応しており、通常どおり処理されます。

OpenMP 特殊関数

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

<JVM システム> 関数

ユーザー表現では、<JVM システム> 関数は、JVM ソフトウェアが Java プログラムの実行以外のアクションを行うために使用した時間を示します。JVM ソフトウェアは、ガベージコレクションや HotSpot コンパイルなどのタスクを、この時間間隔内で実行します。<JVM システム> はデフォルトで関数リストに表示されます。

<Java 呼び出しスタックが記録されていません> 関数

<Java 呼び出しスタックが記録されていません> 関数は <未知> 関数に似ていますが、Java スレッドだけが対象で、Java ユーザー表現でのみ表されます。コレクタが Java スレッドからイベントを受信すると、ネイティブスタックを展開し、JVM ソフトウェアを呼び出して対応する Java スタックを取得します。その呼び出しが何らかの理由で失敗すると、擬似関数 <Java 呼び出しスタックが記録されていません> でアナライザ内にイベントが表示されます。JVM ソフトウェアが呼び出しスタックの報告を拒否する可能性があるのは、デッドロックを回避するためか、Java スタックを展開すると過剰な同期化が発生するときです。

<切り詰められたスタック> 関数

呼び出しスタックの個々の関数のメトリックを記録するためにアナライザが使用するバッファーのサイズは制限されています。呼び出しスタックのサイズが大きくなってバッファーが満杯になった場合に、呼び出しスタックがそれ以上大きくなると、アナライザは関数のプロファイル情報を減らすようになります。ほとんどのプログラムでは、排他的 CPU 時間の大部分はリーフ関数に費やされるため、アナライザは、エントリ関数 _start() および main() を始めとするもっとも重要度の低いスタック下部の関数のメトリックをドロップします。ドロップされた関数のメトリックは、1 つの擬似関数 <切り詰められたスタック> にまとめられます。<切り詰められたスタック> 関数は、Java プログラムでも表示される場合があります。

<合計> 関数

関数<合計> 関数は、プログラム全体を表すために使用される擬似的な構造です。すべてのパフォーマンスメトリックは、呼び出しスタック上の関数のメトリックとして加算されるほかに、<合計> という特別な関数のメトリックに加算されます。この関数は関数リストの先頭に表示され、そのデータを使用してほかの関数のデータの概略を見ることができます。呼び出し元 - 呼び出し先リストでは、任意のプログラム実行のメインスレッドにおける _start() の名目上の呼び出し元、また作成されたスレッドの _thread_start() の名目上の呼び出し元として表示されます。スタックの展開が不完全であった場合、<合計> 関数は、<切り詰められたスタック> の呼び出し元として表示される可能性があります。

ハードウェアカウンタオーバーフロープロファイルに関連する関数

次の関数は、ハードウェアカウンタオーバーフロープロファイルに関連します。

インデックスオブジェクトへのパフォーマンスデータのマッピング

インデックスオブジェクトは、各パケットに記録されたデータからインデックスを計算できる要素のセットを表します。事前定義されているインデックスオブジェクトセットは、スレッド、CPU、標本、秒です。その他のインデックスオブジェクトは、er_print indxobj_define コマンドから直接実行するか、.er.rc ファイル内で定義できます。IDE でインデックスオブジェクトを定義するには、「表示」メニューから「データ表示方法を設定」を選択し、「タブ」タブを選択し、「Add Custom Index Object」ボタンをクリックします。

パケットごとにインデックスが計算され、パケットに関連付けられているメトリックが、そのインデックスのインデックスオブジェクトに追加されます。インデックス -1 は、<未知> インデックスオブジェクトにマップしています。インデックスオブジェクトの階層表示は意味がないので、インデックスオブジェクトのメトリックはすべて排他的メトリックです。

プログラムデータオブジェクトへのデータアドレスのマッピング

メモリー演算に対応するハードウェアカウンタイベントからの PC が、原因と思われるメモリー参照命令にうまくバックトラックするように処理されると、アナライザは、コンパイラからハードウェアプロファイルサポート情報内に提供された命令識別子と記述子を使用して、関連するプログラムデータオブジェクトを生成します。

データオブジェクトという用語は、ソースコード内のプログラム定数、変数、配列、構造体や共用体などの集合体のほか、別個の集合体要素を示す場合に使用します。データオブジェクトのタイプとそのサイズはソース言語によって異なります。多くのデータオブジェクトの名前は明示的にソースプログラム内で付けられますが、名前が付けられないものもあります。データオブジェクトの中には、ほかの単純なデータオブジェクトから生成または集計され、より複雑なデータオブジェクトの集合になるものもあります。

各データオブジェクトは、1 つのスコープへ関連付けられています。スコープとは、そのオブジェクトが定義され、そのオブジェクトを参照できるソースプログラムの領域のことで、それは大域的な領域となる場合 (ロードオブジェクトなど)、特定のコンパイルユニットになる場合 (オブジェクトファイル)、または関数となる場合があります。同一のデータオブジェクトを異なるスコープで定義したり、特定のデータオブジェクトを異なるスコープで異なる方法で参照することができます。

バックトラッキングを有効にして収集された、メモリー操作に関するハードウェアカウンタイベントからのデータ派生メトリックは、関連するプログラムのデータオブジェクトタイプに属するものとされ、そのデータオブジェクトを含む集合体と、<未知><スカラー> も合わせてすべてのデータオブジェクトを含むと見なされる <合計> 擬似データオブジェクトに伝搬します。<未知> のさまざまなサブタイプは、<未知> の集合体まで伝搬します。以降の節では、<合計><スカラー><未知> の各データオブジェクトについて説明します。

データオブジェクト記述子

データオブジェクトは、宣言された型と名前の組み合わせで完全に記述できます。単純なスカラーデータオブジェクト「{int i}」は、型「int」の変数「i」を記述するのに対して、「{const+pointer+int p}」は、「p」と呼ぶ型「int」への定数ポインタを記述します。型名のスペースは「_」(アンダースコア) と置き換えられ、名前の付いていないデータオブジェクトは「-」(ハイフン)、たとえば、「{double_precision_complex -}」という名前で表されます。

集合体全体も同様に、「foo_t」型の構造体の場合は「{structure:foo_t}」と表されます。集合体の要素は、その要素のコンテナを追加指定する必要があります。たとえば、直前の「foo_t」型の構造体の型「int」のメンバー「i」の場合は「{structure:foo_t}.{int i}」となります。集合体はそれ自体、(さらに大きい) 集合体の要素になることも可能で、対応する記述子は集合体記述子を連結したもの、最終的にはスカラー記述子になります。

完全修飾された記述子は、データオブジェクトを明確にするために必ずしも必要ではありませんが、データオブジェクトの識別を支援するために一般的な完全指定を示します。

<合計> データオブジェクト

<合計> データオブジェクトは、プログラムのデータオブジェクト全体を表すために使用される擬似的な構造です。あらゆるパフォーマンスメトリックは、異なるデータオブジェクト (およびそのオブジェクトが属する集合体) のメトリックとして加算されるほかに、<合計> という特別なデータオブジェクトに加算されます。このデータオブジェクトはデータオブジェクトリストの先頭に表示され、そのデータを使用してほかのデータオブジェクトのデータの概略を見ることができます。

<スカラー> データオブジェクト

集合体要素のパフォーマンスメトリックは関連する集合体のメトリック値にさらに加算されますが、すべてのスカラー定数および変数のパフォーマンスメトリックは擬似的な <スカラー> データオブジェクトのメトリック値にさらに加算されます。

<未知> データオブジェクトとその要素

さまざまな状況下で、特定のデータオブジェクトにイベントデータをマップできない場合があります。このような場合、データは <未知> という特別なデータオブジェクトと次に説明するいずれかの要素にマップされます。

メモリーオブジェクトへのパフォーマンスデータのマッピング

メモリーオブジェクトは、キャッシュ行、ページ、およびメモリーバンクなど、メモリーサブシステム内のコンポーネントです。このオブジェクトは、記録された仮想アドレスや物理アドレスから計算されたインデックスから決定されます。メモリーオブジェクトは、仮想ページおよび物理ページについて 8K バイト、64K バイト、512K バイト、および 4M バイトのサイズで事前定義されています。それ以外は、er_print ユーティリティーで mobj_define コマンドによって定義できます。また、アナライザの「メモリーオブジェクトを追加」ダイアログボックスを使用してカスタムメモリーオブジェクトを定義することもできます。このダイアログボックスを開くには、「データ表示方法の設定」ダイアログボックスで「カスタムオブジェクトを追加」ボタンをクリックします。