次の方法で共有


.NET プロファイリング

高レベル ラッパー クラスによるプロファイラの容易な記述

Joachim H. Frohlich and Reinhard Wolfinger


この記事で取り上げる話題:

  • 低レベルの CLR プロファイリング API の内部処理
  • サンプルのプロファイラ ターゲット アプリケーションの概要
  • 高レベル プロファイリング API の作成
  • 高レベルおよび低レベル API のブリッジ

この記事で使用する技術:

  • .NET Framework、COM、C++

この資料は、Microsoft Windows コード ネーム "Longhorn" の Pre-PDC ビルドを基に執筆されており、ここに記載されている情報は変更になることがあります。

サンプルコードのダウンロード: NETProfiling.exe (304KB)
翻訳元: Write Profilers With Ease Using High-Level Wrapper Classes (英語)


目次

  1. 低レベル CLR プロファイリング API
  2. サンプルのプロファイラ対象
  3. 高レベル プロファイリング API
  4. API のブリッジ
  5. まとめ

共通言語ランタイム (CLR) のプロファイリング API は、典型的な機能およびメモリ プロファイラが必要とする以上のサービスを提供します。Microsoft .NET Framework 2.0 のプロファイリング API の最新の拡張機能により、この機能がさらに拡張されます。これらの拡張機能により、ツールは、クラス レベルのメソッド呼び出しだけではなく、アセンブリ境界を越えたデータ フローも追跡することができるようになり、追跡を行うために共通中間言語 (CIL) コードを実装する必要がなくなります。

ただし、この API の強力な機能および詳細な技術情報によって、この簡潔性と実用性が損なわれます。CLR プロファイリング API を使用したプログラミングは、手間がかかり誤りを起こしやすい場合があります。それは、多数のサービスが、低レベルな抽象概念における機能的な設計を主として適用するインターフェイスで構成されているためです。

この記事では、高レベルで理解しやすく、適用しやすい CLR プロファイリング API に基づくプロファイリング API について示します。これにより、基本的なプロファイリング データをフィルタして、それらを意味的に整合性のある概念に再結合するために必要な仕組みを隠蔽します。高レベルの API を使用したプログラミングによって、抽象レベルを .NET Framework の高レベル言語の抽象レベルに近づけることができるため、さまざまな種類のプログラムをプロファイルするツールの構築にかかる開発作業を大幅に削減することができるはずです。

このようなツールの典型的な例には、メソッド呼び出しツリーを再構築して、呼び出しツリーのさまざまなレベルでの実行時間を集計するプロファイラがあります。別の手間のかかる例としては、テストが行われるアセンブリの実際の使用シナリオに基づく確率テストなどの、テスト ドライバ フレームを生成するプロファイラがあります。サンプルのトレーサ (後者の種類のプロファイラの 1 つ) を使用して、高レベル API について説明します。このプロファイラの仕事は、コンポーネント インターフェイス間のデータ フローおよびコントロールを追跡することです。

低レベル CLR プロファイリング API の設計について簡単な復習から始めましょう。API 機能の詳細な説明については、"API リソースのプロファイリング" サイド バーを参照してください。

プロファイラの観点から低レベル API の複雑さを明らかにするアプリケーション例を使用して、設計について説明します。このアプリケーションでは、データ構造を検査するために API に最近追加された機能と、この機能とコントロールおよびデータ フローを追跡するための API 機能との相互関係に焦点を当てます。この場合に低レベル API が要求する詳細な技術情報が邪魔することで、直接的に高レベル API を提案して、この API を使用するプロファイラの実装を説明することになります。

最後に、高レベル API と低レベル API をブリッジする実装の概要について述べます。プロファイルが行われるサンプル アプリケーション (単純な銀行シミュレーション) およびいくつかのプロファイル例とともに使用する高レベル プロファイラの完全なソース コードは、この記事のコード ダウンロードで利用可能です。

ページのトップへ


1. 低レベル CLR プロファイリング API

CLR は、高レベル プログラム向けの使用しやすい、オブジェクト指向のプラットフォームを提供します。CLR サービスの一覧には、アセンブリ、オブジェクト、メソッド起動レコード、スレッド、例外、およびメタデータが含まれます。これらのサービスは、記述されている開発言語にかかわらず、すべての .NET Framework ベースのプログラムの基本的な単位に関係します。

低レベル CLR プロファイリング API によって、プログラムの正確な動作が CLR サービスの状態の変化として表現されます。これらの状態の変化に関する通知を受け取って実用的な方法で処理するために、プロファイラはインプロセスの COM サーバーとして実装されなければなりません。これは、低レベル CLR プロファイリング API が ICorProfilerCallback2、ICorProfilerInfo2、および IMetaDataImport2 という 3 つの COM インターフェイスに及ぶためです (IMetaDataImport2 は、実際はメタデータ API の一部であり、CLR を対象とするアンマネージ ツール (逆アセンブラ、コンパイラ、オブファスケイタ、デバッガ、およびプロファイラを含む) によって使用されます。図 1 は、これらのインターフェイスについて説明します。

図 1 API インターフェイスのプロファイリング

インターフェイス 説明
ICorProfilerCallback2 プロファイラが実装し、プロファイルされるアプリケーションの状態変化をプロファイラに通知するために CLR が使用するコールバック インターフェイス。
ICorProfilerInfo2 CLR が実装し、CLR 構造に関するプロファイルされるアプリケーションの現在の状態をナビゲートするためにプロファイラが使用するインターフェイス。
IMetaDataImport2 CLR が実装し、CLR 構造を解釈するためにプロファイラが使用するインターフェイス。

.NET Framework での高レベルの概念に対する概念的な距離を強調する、低レベルの CLR プロファイリング API について詳しく見てみましょう。たとえば、これらの 3 つの COM インターフェイスは、171 個の関数と 718 個のパラメータにより構成されます。アセンブリ、クラス、およびメソッドなどの上位の概念は、トークン (UINT のポインタ) によりアドレス指定されます。プロファイルが行われるアプリケーションの構造化メソッドのパラメータは、複雑な解釈手順を必要とします。COM インターフェイスに渡される項目のエラー チェックは最小限であり、プロファイラが誤ったトークンを提供すると非常に大規模なクラッシュが発生することになります。さらに CLR は、メソッドの入力パラメータおよびメソッドに入るときの出力パラメータのみのアドレスを提供します。出力パラメータを評価するには、プロファイラは別々のシャドウ ランタイム スタックの管理が必要です。

CLR プロファイリング API コンテキストのオブジェクト アドレスは、実際のオブジェクトを参照しません。これは、ガベージ コレクションおよびアドレスの再利用中に CLR のヒープ管理によりオブジェクトが移動されるためです。プロファイラは、物理的なオブジェクト メモリ内の移動を追跡しなければなりません。CLR の将来のリリースでは、ファイバも使用可能となるかもしれません。ファイバ モードでの実行は、ランタイム スレッドと特定のオペレーティング システムのスレッドとの関連付けを解除する CLR に相当します。これは、フリースレッドのプロファイラの実装向けのスレッドローカルな記憶装置を遅延させます。これらの点は、変数アドレスのマッピングに戻り、高レベルの .NET を対象とする言語が適用する実行モデルの回復を歪めます。

アセンブリ、クラス、およびメソッドなどの構造体にアクセスするために統合 COM インターフェイスではなく関数および UINT ポインタを使用するのはなぜでしょうか。理由として効率性があります。しかし、インプロセス COM サーバーのインターフェイス間のメソッド呼び出しは、C++ の vtbl によるメソッド呼び出しよりも負担がかからず、CLR へのすべての呼び出しがひとまず COM インターフェイス間で行われます。そのため効率性は、低レベル API が COM インターフェイスではなく UINT ポインタとともに動作する理由とはなりにくいです。別の理由として、AddRef および IUnknown の Release メンバを用いたオブジェクトの協同ライフタイム制御に対する COM のアプローチがあります。これによって、非常に動的なシナリオで理解しがたいエラーが引き起こされます。COM 規則に違反するプロファイラは、CLR を簡単に混乱させる可能性があります。


2. サンプルのプロファイラ対象

プロファイラが COM ステートメントと機能面で C 形式のアプリケーション ロジックを混合させて重いコードを生成する方法を説明するために、銀行アプリケーションの例を示します。このプログラムは C# で記述されており、支店、口座、および顧客を持つ単純な銀行をシミュレーションします。図 2 は、銀行アプリケーションのアーキテクチャを示し、どの CLR およびプロファイラが含まれるかを示します。明確にするため mscorlib.dll のような標準アセンブリは省略されます。

図 2 銀行アプリケーションのアーキテクチャ
図 2 銀行アプリケーションのアーキテクチャ

クライアント側のコードの詳細で、より厳密に調査する価値のあるものがいくつかあります。たとえば、以下のコードは銀行の支店および対照口座をセット アップします。

IBank bank = CBank.Get();

IBankBranch bankBranch;

bank.Provide(out bankBranch);

IAccount target = bankBranch.SetupAccount(); // 初期残高 0.00 USD

図 3 のコードは、2 つ目の口座を持っている口座保持者 (顧客) を複数セット アップします。このコードでは、すべての顧客がさまざまな金額を対象口座に同時に振り込みます (これはマルチスレッド アプリケーションです)。

foreach (CCustomer client in customers)
client.StartTransaction();</code></pre>

図 3 口座のセット アップ

ArrayList customers = new ArrayList();

// Smith 氏の口座をセット アップする

CCustomer client = new CCustomer("Smith", bankBranch);

client.BankAccount1 = bankBranch.SetupAccount(

bank.GetAmount(100.00, &quot;USD&quot;)); 

client.BankAccount2 = bankBranch.SetupAccount(

bank.GetAmount(200.00, &quot;USD&quot;)); 

client.TargetAccount = target;

client.AmountToTransfer = bank.GetAmount(280.00, "USD");

customers.Add(client);

// Jones 氏の口座をセット アップする

client = new CCustomer("Jones", bankBranch);

client.BankAccount1 = bankBranch.SetupAccount(

bank.GetAmount(800.00, &quot;USD&quot;));

client.BankAccount2 = bankBranch.SetupAccount(

bank.GetAmount(400.00, &quot;USD&quot;));

client.TargetAccount = target;

client.AmountToTransfer = bank.GetAmount(1100.00, "USD");

customers.Add(client);

...

図 4 は、複数の振り込み元口座からお金を受け取り、それを別のスレッドの振込先口座に振り込む顧客を示します。

図 4 CCustomer クラス

using System.Threading;

internal class CCustomer

{

private IBankBranch _bankBranch;

private IAccount _account1;

private IAccount _account2;

private IAccount _target;

private IAmount _amountToTransfer;

private Thread _thread;

internal CCustomer(string name, IBankBranch bankBranch)

{

_bankBanch = bankBranch;

_thread = new Thread(new ThreadStart(Transfer));

_thread.Name = name;

}

internal string Name { get { return _thread.Name; } }

internal IAccount BankAccount1

{

set { _account1 = value; account.Owner = this.Name; }

get { return _account1; }

}

internal IAccount TargetAccount

{

set { _target = value; }

get { return _target; }

}

internal IAmount AmountToTransfer

{

set { _amountToTransfer = value; }

get { return _amountToTransfer; }

}

internal void StartTransaction() { _thread.Start(); }

private void Transfer()

{

_bankBranch.Transfer(_amountToTransfer,

  _account1, _account2, _target);

}

...

}

プロファイラは、メソッド境界を越えて渡されるデータとアプリケーションの制御フローを記録する必要があります。低レベル CLR プロファイリング API の興味深い新機能の 1 つとして、メソッド パラメータの解釈があります。構造化パラメータおよび非構造化パラメータ両方の名前、種類、値を抽出するには、プロファイラは CLR からのコールバック中に複数の低レベル API 関数に接続しなければならず、これにより、メソッド起動 (入る時) あるいはメソッド終了 (出る時) のいずれかにおける、メソッド境界を越える通常あるいは例外制御フローを知らせます。

この記事に付随するダウンロードにあるソース ファイル LowLevel.cpp のコードの量は、非構造化および構造化メソッド パラメータ一覧の名前、種類、および値をダンプするために、低レベル API で行わねばならないことに関してある印象を与えます。たとえば、金額の合計を構造化パラメータとして表します。サンプルのアプリケーションでは、オブジェクトは通貨単位および浮動小数点通貨額から構成されます。

多くのパラメータを指定してある関数を呼び出すときに、COM 機能の HRESULTs および必要のないパラメータのダミー値を処理するために必要なステートメントの保護といった詳細な技術情報により、低レベル プロファイリング API を使用するときのもどかしさが示されます。さらに、この実装レベルにおいて、オブジェクト識別子に戻る CLR のガベージ コレクタにより管理される物理的オブジェクトのアドレスのマッピングのように、複数のサービスは単に利用できません。これを動作させるには、プロファイラは各オブジェクトのメモリ アドレスを、そのライフタイム中追跡しなければなりません。

ページのトップへ


3. 高レベル プロファイリング API

テスト ドライバのジェネレータ、モニタ、ビジュアライザ、プロトコル チェッカなどのさまざまな種類のプロファイリング ツールはすべて、コンポーネントおよびオブジェクトに関する .NET Framework ベースのプログラムの動作を再構築するのと同様の方法で、低レベル CLR プロファイリング API を適用します。これらすべてのツールは、.NET で (ICorProfilerInfo2 を使用して) 記述されるプログラムの動作を反映するランタイム データと、このデータの解釈を (IMetaDataImport2 によって) 可能とするデータを組み合わせます。一般的に、プロファイリングは、スレッドおよび関数あるいはメソッドの追跡によって始まります。したがって、スレッドおよびメソッドはともに、図 5 に示すように構造化プロファイリング インターフェイスのルートを構成します。図 6 は、このインターフェイスを、プロファイリング アーキテクチャ全体で示します。

図 5 高レベル プロファイリング インターフェイス
図 5 高レベル プロファイリング インターフェイス

図 6 低レベルおよび高レベル プロファイリング インターフェイス
図 6 低レベルおよび高レベル プロファイリング インターフェイス

高レベル インターフェイスは、プロファイルが使用すると思われる方法に従って設計されます。インターフェイスは、IEvents および IMethod などのいくつかのサブインターフェイスにより構成されます。IEvents は、スレッドを作成する CLR、メソッドに入る通常の制御フロー、およびメソッドに入る例外的制御フローなどのプロファイルが行われるアプリケーションの状態の変化を知らせるための、ICorProfilerCallBack2 と同等の高レベル インターフェイスです。IMethod は、実行しているスレッド、実行コンテキスト (オブジェクト)、すべてのパラメータ、および (もしあれば) 結果を取得するための、ICorProfilerInfo2 および IMetaDataImport2 の選択された部分を凝縮した高レベル インターフェイスです。これらのインターフェイスのそれぞれが、純粋な抽象クラスとして実装されますが、実際は、IEvents を用いて示されるアクセス ラベル (パブリック) を保存するだけの構造体です。高レベル API のクライアントは、プロファイルが行われるアプリケーションでの状態の変化について通知を受けるために IEvents を実装しなければなりません。

interface IEvents // インターフェイス構造体の #define

{

virtual void ThreadCreated(const IThread *thread) = 0; //略して "tc"

virtual void ThreadDestroyed(const IThread *thread) = 0; //td

virtual void NormalFlowEntered(const IMethod *method) = 0; //nfe

virtual void NormalFlowLeft(const IMethod *method) = 0; //nfl

virtual void ExceptionThrown(const IObject *exception) = 0; //et

virtual void ExceptionalFlowEntered(

const IMethod *method, const IObject *exception) = 0; //efe

virtual void ExceptionalFlowLeft(

const IMethod *method, const IObject *exception) = 0; //efl

...

};

このコードが示すように、高レベル プロファイリング API を使用したプログラミングは、プロファイリング アプリケーションが必要とするものにより適合していて、低レベル CLR プロファイリング API を使用するときより誤りを起こしにくくなければなりません。この利便性の理由として、高レベル API の意味的に豊かな構造があります。高レベル API には、低レベルでのハンドルおよびトークンの代わりとなる複数の高レベル インターフェイスが、タイプセーフで豊かな表現方法で用意されています。図 7 で示されるプロファイラ アプリケーションの抜粋について考えてみましょう。著しく少量のステートメントによって、LowLevel.cpp の低レベル API を使用して実装されたプロファイラよりも多くのことを行っています。低レベルの改良型のように、高レベルの API は、前者の種類の構造体および配列と同様に、文字列、倍精度浮動小数点数型、およびオブジェクトをダンプします。さらに、低レベル プロファイラが、オブジェクトのアドレス (ガベージ コレクションに起因して、プロファイルが行われるアプリケーションのメソッドの実行中に変更されます) のダンプのみを行うことができる一方、高レベル プロファイラは、アプリケーション セッションに固有の番号を持つオブジェクトを識別します。オブジェクト識別子は、.NET を対象とするプログラミング言語のレベルでのオブジェクトの識別方法をより直接的に反映します。低レベル API の呼び出しは完全に隠蔽されるという重大な違いがありますが、簡単に比較するために、高レベル プロファイラの関数呼び出しグラフは、低レベル プロファイラのグラフに対応しています。したがって、プロファイラ アプリケーションは自身の処理に集中することができます。

図 7 高レベル プロファイラ API

void MsdnTracer::NormalFlowEntered(const IMethod *method)

{

TraceParameterList(method);

}

void MsdnTracer::TraceParameterList(const IMethod *method)

{

for(int i=0; i < method->GetArgumentCount(); i++)

TraceParameter(method-&gt;GetArgument(i).get());

if(method->GetArgumentCount() > 0) _tprintf(_T("\n"));

}

void MsdnTracer::TraceParameter(const IArgument *arg)

{

IMemoryPtr value = arg->GetValue();

// typedef std::auto_ptr&lt;IMemory&gt; IMemoryPtr;

CString direction;

if(arg->IsIn()) direction = _T("in");

else if(arg->IsOut()) direction = _T("out");

else direction = _T("ref");

_tprintf(

_T(&quot;High-level API: [ %s]  %s %s  %s=&quot;), 

direction, arg-&gt;GetTypeName(),

arg-&gt;IsArray() ? _T(&quot;[]&quot;) : _T(&quot;&quot;), arg-&gt;GetName());

TraceValue(arg->GetValue().get());

}

void MsdnTracer::TraceValue(const IMemory *value)

{

const IPrimitive *pri;

const IString *str;

const IStructured *obj;

if((pri = dynamic_cast<const IPrimitive*>(value)) != NULL

&amp;&amp; pri-&gt;GetDatatype() == typeDouble)

TraceDouble(pri);

else if((str = dynamic_cast<const IString*>(value)) != NULL)

TraceString(str);

else if((obj = dynamic_cast<const IStructured*>(value)) != NULL)

TraceStructured(obj);

else

_tprintf(_T(&quot;$Unsupported&quot;));

}

void MsdnTracer::TraceDouble(const IPrimitive *value)

{

_tprintf(_T(" %f"), value->GetDouble());

}

void MsdnTracer::TraceString(const IString *value)

{

_tprintf(_T("# %d &quot; %s&quot;"), value->GetLogicalId(),

value-&gt;GetText());

}

void MsdnTracer::TraceStructured(const IStructured *value)

{

_tprintf(_T("# %d {"), value->GetLogicalId());

for(int i=0; i < value->GetSize(); i++)

{

IMemoryPtr field = value-&gt;GetAt(i);

if(i &gt; 0) _tprintf(_T(&quot;, &quot;));

_tprintf(_T(&quot; %s %s  %s=&quot;), field-&gt;GetTypeName(),

  field-&gt;IsArray() ? _T(&quot;[]&quot;) : _T(&quot;&quot;), field-&gt;GetName());

TraceValue(field.get());

}

_tprintf(_T("}"));

}

高レベル プロファイラである HighLevel.cpp (図 7 に示されます) は、本来は次のように動作します。クラス MsdnTracer はプロファイリング アプリケーションのエントリ ポイントです。したがって、MsdnTracer は、メソッドに入る制御フローに反応するために、コールバック インターフェイス IEvents の NormalFlowEntered メソッドを実装します。

NormalFlowEntered(IMethod *method)

基本的なパラメータ (たとえば IPrimitive あるいは IString) の場合は、MsdnTracer はそれをダンプして次のパラメータに移動します。パラメータが複数のコンポーネントから成る場合 (たとえば、IStructured)、MsdnTracer はそれらのコンポーネントでループして、種類、名前、および各値をダンプします。

for(int i=0; i < value->GetSize(); i++)

{

IMemoryPtr field = value->GetAt(i);

if(i > 0) _tprintf(_T(", "));

_tprintf(_T(" %s %s %s="), field->GetTypeName(),

field-&gt;IsArray() ? _T(&quot;[]&quot;) : _T(&quot;&quot;), field-&gt;GetName());

TraceValue(field.get());

}

このループは、共通のスーパータイプである IStructured を使用してすべての種類の構造、つまり配列 (IArray)、構造体 (IRecord)、およびオブジェクト (IObject) を統一したタイプセーフな方法でカバーするため、包括的に実装されます。これには、コンポーネント (メンバ変数など) が継承パスに従って複数のクラスによって定義されているオブジェクトが含まれます。メソッド TraceValue の再帰呼び出し中に、IStrucured の構造化サブタイプの 1 つに属するコンポーネントがダンプされます。さらに、MsdnTracer は、プロファイラの適切なサービス、低レベル API で利用できないサービスを呼び出すことで、文字列の論理識別子 (オブジェクトとして) およびユーザー定義構造体の論理識別子をダンプします。

_tprintf(_T("# %d \" %s\""), value->GetLogicalId(), 

value->GetText());

...

_tprintf(_T("# %d {"), value->GetLogicalId());

高レベル プロファイラ アプリケーションが、低レベル CLR プロファイリング API を直接実装しているプロファイラ アプリケーションよりも管理が簡単で、小さく、より機能的で、より読みやすく、より強固であることに注意してください。出力をより複雑な方法で書式設定する、高レベル プロファイリング アプリケーションの改良型は、ダウンロードにある App.Tracer\IndentedTextFormatter.cpp で利用可能です。この改良型は、ダウンロードにあるディレクトリ Bank.log に含まれているようなログを記録します。

図 5 の高レベル プロファイリング API の一例を調査すると、インターフェイスおよび名前空間のモデリング、例外処理などを含むランタイム モデルの設計についていくつか疑問を抱くかもしれません。実際のアプリケーション向けの表現力が豊かなログあるいはテスト ドライバの設計者は、このような疑問を避けることはできないため、作成しているプロファイリング API の設計の決定事項に関して回答を提供します (さらに、読者は設計の決定事項のすべてに同意するわけではないかもしれません)。

名前空間のモデリングを行いました。これは厳密に言うと、IMethod のプロパティとしてクラスおよびインターフェイスを含みます。これにより、名前空間に基づく関連するメソッド起動のフィルタリングが容易になります。メソッドがプログラムの動作のプロファイリングで果たす中心的な役割のため、この決定は適切です。

メソッドに入る例外を、例外についての情報を提供するオブジェクトを任意で伝える逆制御フローとしてモデリングを行いました。図 8 は、このことを、複数のスタック層 (ET1 から EFE2) の間の例外的な制御フローを記述するイベントを用いて示します。

図 8 スタック フロー制御
図 8 スタック フロー制御

ページのトップへ


4. API のブリッジ

高レベルの API の表現の豊かさは、もちろんただで生じるわけではありません。これは単純に、高レベル API では次の項目を含む、オブジェクト指向の抽象レベルにおいてプログラムに関するさまざまなビューを再構築するために必要なサービスを、プロファイラが提供しているためです。

  • 関連するアセンブリ、クラス、およびインターフェイスのオブジェクト向けイベントのフィルタリング
  • ある深度より深いコール スタックの切断
  • .NET ベースのオブジェクトの、そのライフタイム中のさまざまなメモリ位置の追跡
  • オペレーティング システム スレッドの .NET Framework でのスレッドへの再マッピング
  • メソッドに入る時とメソッドを出る時に、スタック上のメソッドのパラメータに等しくアクセスするための管理
  • 基本的なメソッドおよび構造化メソッドのパラメータの解釈を簡単に行うための手続きの提供

これは大変な作業であるため、どのようにすべての部品を 1 つにまとめたらよいかと考えてしまうかもしれません。プロファイラを構成するために、Config.cmd に示されるような環境変数をいくつか設定しなければなりません (図 9 を参照)。ここで検討しているプロファイラは、具体的な銀行での実装の使用シナリオを記録するということを思い出してください。この構成では、プロファイラはダウンロードにあるディレクトリ Bank.log にログを作成します。これらのログは、プロファイルが行われるアプリケーションのテストおよびデバッグをサポートします。

図 9 セクション プロファイラ構成

@REM *** Section 1: configure the CLR profiler

@set COR_ENABLE_PROFILING=1

@regsvr32 /s Spyder.dll

@set COR_PROFILER={B98BC3F3-D630-4001-B214-8CEF909E7BB2}

@REM *** Section 2: configure the profiler core

@set SPYDER_TARGET_PROCESS=BankApplication.exe

@set SPYDER_STACK_DEPTH=1 & REM trace only direct method calls

@set SPYDER_FILTER=C:{Bank.Interface}.* ^| I:{Bank.Interface}.*

& REM ?or' in the sense of set union

@set SPYDER_TARGET_ASSEMBLY=BankInterface;Bank

@set SPYDER_APPLICATION=Tracer

@REM *** Section 3: configure the profiler application (here: Tracer.dll)

@set SPYDER_INSTANCE_DATA=1 & REM dump objects crossing boundaries

@set SPYDER_SHOW_NAMESPACE=0 & REM do not dump namespace names

@set SPYDER_FILE_PER_THREAD=0 & REM all data go to one file

まず最初に、CLR にどのプロファイラをロードするかを指示します。今回の場合、プロファイラは実際にはプロファイラ コアとして参照する Spyder.dll にあります。

@set COR_ENABLE_PROFILING=1

@regsvr32 /s Spyder.dll

@set COR_PROFILER={B98BC3F3-D630-4001-B214-8CEF909E7BB2}

2 番目のセクションで、プロファイラのアプリケーションから独立した部分に対して、プロファイルが行われる重要なアプリケーション BankApplication.exe のみをフックするよう指示します。ここでプロファイラは、名前空間 Bank.Interface のインターフェイスあるいはクラスで宣言された上位レベルすべてのメソッドの起動を記録します。

@set SPYDER_STACK_DEPTH=1    & REM trace only direct method calls 

@set SPYDER_FILTER=C:{Bank.Interface}.* ^| I:{Bank.Interface}.*

& REM ?or' in the sense of set union

コードから推測されるように、名前空間 Bank.Interface がアセンブリ BankInterface.dll に制限されていることにより、銀行アプリケーションと具体的な銀行の実装とを完全に分離します (図 2 を参照)。プロファイラを加速させるために、まさにこれらの 2 つのアセンブリ (9 行目を参照) に対してプロファイラの注意を向けるよう制限します。これは、それら 2 つが一緒にプロファイリングの目標に関連する部分を構成するためです。

@set SPYDER_TARGET_ASSEMBLY=BankInterface;Bank

次に、プロファイラ コアに対して、プロファイラのアプリケーションに依存した部分を、3 番目のセクションで構成される可能なプロファイラ アプリケーションである Tracer.dll からロードするよう指示します。

@set SPYDER_APPLICATION=Tracer

この場合、銀行アプリケーションと銀行の実装との間でやりとりされるオブジェクトの構造をダンプするように、トレーサをセット アップします。ここで、オブジェクト構造はデモ目的でダンプされることに注意してください。

@set SPYDER_INSTANCE_DATA=1 & REM dump objects crossing boundaries

通常、オブジェクト識別子をダンプする (set SPYDER_INSTANCE_DATA=0) だけで十分です。なぜなら、それらの内部構造は、銀行の例での場合のように、完全にクライアントから隠蔽されているためです。さらに、プロファイラ アプリケーションは、名前空間にログを圧縮させることなく、クラスおよびインターフェイス名をダンプします。

@set SPYDER_SHOW_NAMESPACE=0 & REM do not dump namespace names

ある特定のセッション中のプログラムの動作のグローバルなビューを取得したいので、プロファイラに対して、すべてのスレッドについてのデータを 1 つのログ ファイルにダンプするよう指示します。

@set SPYDER_FILE_PER_THREAD=0 & REM all data go to one file

最後の点で、効率性のボトルネックが示されます。トレーサのレスポンス時間を短縮させ、単純な改良型をより優れたソリューションで置き換えようとするかもしれません。このようなトレーサを、たとえば 2 つの部分に分割して、2 フェーズで動作させることができます。最初の部分は、プロファイラ コアに添付されます。

set SPYDER_APPLICATION=YourCleverProfiler.dll

最初のフェーズで、各スレッドの各制御フロー イベントにシーケンシャル番号あるいはタイム スタンプをつけて、データをスレッドごとに別々のファイルにダンプします。2 番目のフェーズで、独立して外部で実行するトレーサの部分がこれらの別々のファイルを結合して、グローバルなビューを取得します。この討論により、直接的にプロファイラの内部動作に導かれます。

プロファイラは、プロファイルが行われるアプリケーションへの影響を最小限に抑えるため、効率的に実装されなければなりません。関数あるいはメソッド間のランタイム サイクルの配分を決定する典型的なプロファイラにとっては、これは本来プロシージャの測定に起因するオーバーヘッドを合計実行時間から差し引くことを意味します。テストあるいはデバッグ目的の追跡としてプログラム セッションを記録するプロファイラにとっては、これは特にタイムアウトを避けることを意味します。プロファイラによって発生するタイムアウトは、プロファイルが行われるアプリケーションの動作を変則的な方法で変更することがあります。したがって、プロファイラは主にランタイムのパフォーマンスに対して最適化されなければなりません。プロファイラのメモリの需要は、あるしきい値を下回り、オペレーティング システムのメモリ マネージャに負担をかけないでいる間は、プロファイルが行われるアプリケーションのワーキング セットを増加させ、またしたがってタイムアウトを回避します。

プロファイラ コアは、可能な限り無駄を排除し、要求に応じてのみ動作しなければならないという 2 つの要件を満たすことを主として設計されます。両方の要件は、主に実行時間を削減することを目的としています。可能な限り時間とスペースの両方を有効に扱う一方で、対立する状況として、時間に対する最小限の影響を支援するよう決定しました。

プロファイラ コアは、データ項目を別々に管理するのではなく、CLR をデータの格納物として使用することで最初の要件を実装します。将来のアクセスを加速させるために、プロファイラ コアが CLR データを変換して変換結果をキャッシュするのは一部の場合のみです。明らかな例として、メソッドおよび名前空間の名称があります。それらの名称は、メソッドに入る時に変換されると、プロファイラ アプリケーションがメソッドから出る時に再度アクセスすることが多いため管理されます。大部分の場合、プロファイラ コアは単純に CLR データ構造内の位置を記憶して、再度の繰り返しを避けます。

プロファイラはこの処理状態の情報を、プロファイラ アプリケーションが IPrimitive または IObject といった高レベル プロファイリング インターフェイスの一部を使用してのみアクセスできるオブジェクトにカプセル化します。図 10 のコードは、高レベル インターフェイス IMethod を実装する Method クラスを引用して、結果の実装を示します。

図 10 Method クラス

IArgumentPtr Method::InternalGetArgument(int index)

{

PCCOR_SIGNATURE sigBlob = NULL;

int argumentCount = this->GetArgumentCount();

// 署名 blob の引数の部分の位置を再利用する

int argIndex = 0;

if(m_argSig.GetSize() == 0)

{

this-&gt;Seek(_Arguments); // 引数の部分に移動する

sigBlob = m_sigBlob[_Arguments]; 

}

else

{

// 引数が既に読み込まれているので、最も近い位置を使用する

argIndex = min(index, m_argSig.GetSize()-1); 

sigBlob = m_argSig[argIndex]; // 最も近い引数の位置

}

for(; sigBlob != NULL && argIndex < argumentCount; argIndex++)

{

// sigBlob で引数を記述する部分の

// 開始位置を覚えておく

if((argIndex+1) &gt; m_argSig.GetSize()) 

  m_argSig.Add(sigBlob); 



bool isByRef, isArray; 



CorElementType type = 

  TypeInterpreter::GetType(sigBlob, isByRef, isArray); 

mdTypeDef typeDef = TypeInterpreter::GetTypeToken(sigBlob, type); 



if(argIndex == index) 

  return IArgumentPtr(DEBUG_NEW Argument( 

    this, argIndex, type, isByRef, isArray, typeDef)); 

}

return IArgumentPtr(NULL);

}

このコードは、プロファイラ アプリケーションが、パラメータ一覧の位置によって、プロファイルが行われるアプリケーションのメソッドのパラメータの引数にアクセスするときに、高レベル プロファイリング API のバックグラウンドで何が実行しているかを示します。このコード ブロックは、IArgument 型のオブジェクトとなり、プロファイラ アプリケーションが問い合わせ名、種類、および値について使用することができます。CLR のインターフェイスをプロファイルすると、プロファイラは目的の引数に到達するまで、引数一覧を記述する CLR データ構造 (図 10 の sigBlob) を繰り返すよう強制されます。プロファイラ コアは、繰り返している間にこのデータ構造のインデックス化を行います。したがって、パラメータ アクセスを繰り返すと、初期アクセス中のような線形時間ではなく定数時間がかかります。

要求に応じた要件によって、プロファイラ コアは怠惰であることが示されます。プロファイラ コアは、クラス Method のようなアクセス オブジェクトのメソッド引数を単にラップして、プロファイラ アプリケーションがそれらにアクセスする必要あるときにパラメータの値を変換するだけではありません。プロファイラ コアは、プロファイルが行われるアプリケーションの動作をオブジェクト指向の .NET 抽象レベルで追跡するために無条件に必要な、それらのタスクのみを自動的に実行します。それらのタスクの 1 つとして、プロファイラ ユーザーが記録目的で選択したコンポーネントのメソッド起動によるマネージング シャドウ スタックがあります。

もう 1 つのタスクは、スレッドおよびオブジェクトなどのエンティティの逆マッピングに関係します。.NET Framework ベースのオブジェクトの場合、CLR メモリ管理はそれらを別の物理的アドレスにマップし、物理的アドレスが別のオブジェクトに割り当てられる可能性があります。スレッドの場合は、CLR が OS の別のスレッドに戻します。これらのマッピングを元へ戻すには、プロファイラ コアは、低レベル OS スレッドと高レベル .NET ベースのスレッド間の関係を管理するテーブル、および低レベル メモリ アドレスと高レベル オブジェクト識別子間の関係を管理するテーブルの 2 つを使用します。マッピング用のテーブルを使用することで、実行時間を一定して使用するようにします。選択されたコンポーネントの使用シナリオを記録する高レベル プロファイラ アプリケーションは、テストドライバのジェネレータが行うように、より小規模なシャドウ スタックとなります。これは、選択されたアセンブリのインターフェイスの上位レベルのメソッド呼び出しのみを観察するためです。プロファイラ アプリケーションが、どのような呼び出しレベルにおいて、どのようなアセンブリの各メソッド呼び出しでも追跡する場合、実際のスタックおよびそれぞれのシャドウ スタックは、メソッド起動の同じ数を管理します。

もしかなり明白であるように思われるとしても、プロファイラ コアが怠惰であることによるもう 1 つの結果を強調しなければなりません。プロファイラ アプリケーションの開発者は、メソッド起動間のパラメータ値に関するプロトコル依存の制約を確認するために、実際には IMethod 型のオブジェクトへの参照である起動レコードを保存したくなるかもしれません。メソッドの起動の終了後 (実際にはプロファイラ コアが、起動されたメソッドの NormalFlowLeft あるいは ExceptionalFlowLeft イベントのいずれかを発生後)、実行に特有のデータは利用できなくなります。プロファイラ コアを拡張すると、プロファイラ アプリケーションによる要求があれば起動レコードの複製を作成し、それらを将来のアクセスに備えて圧縮して保存するというサービスをさらに提供することができます。

高レベル プロファイリング API の設計は、C++ の豊かな表現力の恩恵を受けます。Public のメソッド宣言のみでインターフェイスを記述する手段としての、純粋な抽象構造体については既にお話しました。概念的な観点からすると、もう 1 つの例として、プロファイラはプロファイルが行われるアプリケーションの通常の手順および論理的動作を変更してはいけないため、ランタイム モデルのすべての部分が定数であるという事実があります。ただし、高レベル API での定数としての関数の宣言は、プロファイラ コアが最新の可能な時間で、要求によってのみ動作するための要件とは矛盾します。

C++ では、const_cast を使用して、単に構造体オブジェクトを一時的に編集可能にすることによる怠惰な初期化によって、一方でセキュリティの、他方では効率性の必要性の間の緊張を緩めることができることを覚えておいてください。プロファイラ コアの内部で実行しているサービス オブジェクトは待機して、プロファイラ アプリケーションにより強制されるときのみ状態を変更します。これで、抽象、メソッド IMethod.GetArgument によって例示される高レベル インターフェイス、およびプロファイラ コアの実装 Method.InternalGetArgument の間のギャップを埋めることができます。

interface IMethod // インターフェイス構造体の #define

{

...

virtual IArgumentPtr GetArgument(int index) const = 0;

...

};

この場合の定数キャストは単に、メソッド全体のオブジェクトの型の定数部分を削除するだけです。

public: 
... 

IArgumentPtr GetArgument(int index) const 

{ 

  return (const_cast&lt;Method*&gt;(this))-&gt;InternalGetArgument(index); 

} 

... 

};

ページのトップへ


5. まとめ

この記事は、効率性と抽象概念との間の関係に関する討論と考えることができます。バイナリ インターフェイスに焦点を置く COM、そして包括的な言語機能を持つ C++ の両方が、抽象概念と効率性を調和の取れた方法でまとめるための理想的な手段を提供します。驚くほど強力な .NET を対象とする言語のいずれかで記述されたマネージ プログラムが重要性を増すとしても、この手段はしばらくの間有効であると断言します。

最後に回答すべき質問が残ります。プロファイラ アプリケーションの全体的なパフォーマンスは結局どうなるのでしょうか。これは、プロファイラのタスクとプロファイルが行われるアプリケーションの構造によって明らかに異なります。

全体としてみれば、根本的な問題には複数の次元があります。たとえば、メソッドの署名のみをダンプするプロファイラは、メソッドの引数も再帰的にダンプするプロファイラよりは、明らかに時間がかかりません。最小限の実行時間のオーバーヘッドにより、プロファイラ コアは、さらにデータ フィルタを用いることで多次元の問題を小さくする方法だけではなく、低レベル CLR プロファイリング API で利用できないサービスも提供します。データ フィルタにより、焦点が置かれたプロファイルからの抽出および選択されたアセンブリ向けのテスト ドライバの生成が可能となります。開発者およびテスティング スタッフは、アセンブリ、コンポーネント、および開示されている重要なインターフェイスとクラス、およびそれらの関係といった観点から、常にプログラムの構造を理解していなければなりません。そのためこれらのインターフェイスは簡潔で最小限でなければなりません。もしそうでなければ、データ フィルタが複雑すぎてフィルタの適用負担が増大してしまうか、データ フィルタが狭すぎるか広すぎてあまり役に立たないプロファイルになってしまうかのいずれかです。

専門家は、強く統合されたコンポーネントの適切なインターフェイスが、十分に設計されていてテストを行いやすいプログラムの特徴であることに同意しています。テストを行いやすいプログラムによって、.NET ベースのプログラムの品質を保証する両方のプロファイリング API の力を使用することが可能となります。低レベル CLR プロファイリング API を使用したプログラミングは、あまり構造化されていなく、したがってより柔軟性がありますが、使いにくくもあります。高レベル プロファイリング API を使用したプログラミングは柔軟性に欠けますが、簡単に使用でき、高レベルの .NET を対象とする言語の抽象レベルに近いところで動作するプロファイラが必要とするものに適応します。

ページのトップへ


API リソースのプロファイリング

次の記事および書籍は、CLR プロファイリング API に関してさらに詳細な情報を提供します。

CLR Profiler: どんなコードも見逃さない .NET Framework 2.0 のプロファイリング API
Jay Hilyard 著 MSDN Magazine 2005 年 1 月

Under the Hood: The .NET Profiling API and the DNProfiler Tool (英語)
Matt Pietrek 著 MSDN Magazine 2001 年 12 月

『.NET&Windowsプログラマのためのデバッグテクニック徹底解説』
John Robbins 著 (Microsoft Press、2003 年) 第 10 章「マネージド例外の監視」および第 11 章「フロートレーシング」

ページのトップへ


Joachim H. Frohlich (joachim.froehlich@acm.org) は、オーストリアの Johannes Kepler University of Linz のソフトウェア エンジニアリング学部のメンバで、.NET コンポーネント アーキテクチャのテストおよび構成の容易性に集中して取り組んでいます。


Reinhard Wolfinger (wolfinger@ase.jku.at) は、オーストリアのリンツにある Christian Doppler Laboratory for Automated Software Engineering で研究者として勤務しており、.NET コンポーネント アーキテクチャおよび再利用に注目しています。


この記事は、MSDN マガジン - 2006 年 4 月からの翻訳です。

QJ: 060405

ページのトップへ