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

第 6 章 パフォーマンスアナライザとそのデータについて

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

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

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

データ収集の動作

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

実験の形式

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

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

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

すべてのデータファイルはメモリーマップされ、ブロック単位で書き込まれます。レコードは常に有効なレコード構造を持つように記入されるので、実験は書き込み中に読み取ることができます。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 実験が実行されます。

collect による実験

collect コマンドを使用して実験を記録する場合、collect ユーティリティーは実験ディレクトリを作成し、libcollector.so およびそのほかの libcollector モジュールがターゲットのアドレス空間にあらかじめ読み込まれるように LD_PRELOAD 環境変数を設定します。collect ユーティリティーは、その後、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、または CPU によるメトリックスのフィルタ処理を実行できます。プロセッサ 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 またはスレッドが再びユーザーモードになるまで、呼び出しスタックの内容は変わりません。すなわち、呼び出しスタックには、各プロファイル間隔の最後のプログラムカウンタの位置が常に正確に記録されます。

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

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

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

内容の説明  

メトリック名  

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) のマニュアルページを参照してください。これらのタイミング値と <Total> 関数 (プログラム全体を表す) のメトリックスを比較することによって、集計されたタイミングメトリックスのおおよその精度を知ることができます。ただし、「統計」タブに表示される値には、<Total> のタイミングメトリック値に含まれないそのほかの関連要素が含まれることがあります。その原因は、データ収集が一時停止される期間によるものです。

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

同期待ちトレース

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

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

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

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

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

動作クロック周波数が動的に変わるマシンで記録される実験では、サイクルベースのカウントから時間への変換で不正確さが生じます。

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

ヒープトレース

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

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

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

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

データ空間プロファイリングは、メモリ参照に対して使用されるハードウェアカウンタプロファイリングの拡張版です。ハードウェアカウンタプロファイリングでは、メトリックスをユーザー関数、ソース行、および命令に割り当てることができますが、参照されるデータオブジェクトに割り当てることはできません。デフォルトでは、コレクタはユーザー命令アドレスのみを取得します。データ空間プロファイリングが有効な場合、コレクタはデータアドレスも取得します。バックトラッキングとは、データ空間プロファイリングをサポートするパフォーマンス情報の取得に使用されるテクニックです。バックトラッキングが有効な場合、コレクタは、ハードウェアカウンタイベント前に発生したロード命名またはストア命令に戻って、そのイベントの原因になった可能性のある命令の候補を1つ見つけます。

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

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

引数なしで collect を実行すると、ハードウェアカウンタが一覧表示され、それらが、ロード関係なのか、ストア関係なのか、ロード-ストア関係なのかが示されます。「ハードウェアカウンタオーバーフローのプロファイルデータ」を参照してください。

MPI トレース

MPI トレースは、VampirTrace データコレクタの修正版をベースにしています。詳細は、Technische Universität Dresden Web サイトにある『Vampirtrace User Manual』を参照してください。

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

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

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

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

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

プロセスイメージ

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

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

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

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

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

関数」という用語は、ソースコードで記述された高度な操作を表す一連の命令を表します。この用語は、Fortran で使用されるサブルーチンや、C++ および Java プログラミング言語で使用されるメソッドなども表します。サブルーチン関数 サブルーチンFortranサブルーチンメソッド 関数関数はソースコードで明確に記述され、通常、その名前は、一群のアドレスを表すシンボルテーブル内に出現します。

原則として、ロードオブジェクトのテキストセグメント内のアドレスは関数にマップすることができます。関数ロードオブジェクト内のアドレス ロードオブジェクト関数のアドレス呼び出しスタック上のリーフ 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 関数については、「動的な関数とモジュール」を参照してください。情報を提供しなかった場合、関数は <Unknown> としてパフォーマンス解析ツールに表示されます。

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

<Unknown> 関数

特定の条件では、PC が既知の関数にマップされないことがあります。このような場合、PC は <Unknown> という名前の特別な関数にマップされます。

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

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

OpenMP の特殊な関数

擬似関数は、スレッドが 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 セクションに入る順番を待っているスレッド 

<JVM-System> 関数

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

<no Java callstack recorded> 関数

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

<Truncated-stack> 関数

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

<Total> 関数

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<Total> データオブジェクト

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

<Scalars> データオブジェクト

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

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

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

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

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