[GitHub]からソースをダウンロードします。
ELTProfiler、ConsoleTest プロジェクトを Visual Studio 2022 バージョンでオープンしてビルドします。 その後、Binフォルダにあるrun.batファイルを実行すると、基本的なテストが可能になります。
@echo offSET CORECLR_PROFILER={cf0d821e-299b-5307-a3d8-b283c03916dd}SET CORECLR_ENABLE_PROFILING=1SET CORECLR_PROFILER_PATH=%~dp0ClrProfiler.dllSET PROFILE_BASETIME=1000SET NAMESPACE_PREFIX=ProfilingTestConsoleTest.exe
以下はrun.batファイルの実行結果のスクリーンショットです。tick は PerformanceCounter の単位時間です。現在サンプルとして提供されているプログラムの実行時間が短すぎるため、ミリセカンド単位を使用しませんでした。PerformanceCounterのleaveInMilliseconds()メソッドを使用すると、ミリセカンド単位で測定できます。
Profiler DLL を環境変数に設定すると、.NetのCLRは、Netアプリケーションで発生したイベントを ICorProfilerCallback インターフェイスを実装したオブジェクトに渡します。Profiler APIを使用して構築されたDLLを環境変数に登録し、その後に.Netアプリケーションを実行すると、CLRはそのDLLで定義されたProfilerオブジェクトを生成します。
また、アプリケーションで発生した特定のイベントを Profiler オブジェクトに渡します。このイベントにより、.Netアプリケーションの内部活動とパフォーマンス関連の指標を得ることができるのです。
リポジトリの提供されるサンプルコードではICorProfilerCallback8を使用しています。.Netバージョンが更新されている間に、以前のインターフェイスが拡張され、後ろに数字が増えています。
イベントは.Netアセンブリがロードされたとき、またはメソッドがJITコンパイルを開始する時点など、さまざまに提供されています。例では、.Netアプリケーションが初期化されたときに発生するInitializeイベントを利用して、すべてのメソッドが実行または終了する時点を傍受するようにしています。
HRESULT STDMETHODCALLTYPE CorProfiler::Initialize(IUnknown *pICorProfilerInfoUnk){// ICorProfilerInfo8 インターフェイスでクエリを試みます。HRESULT queryInterfaceResult = pICorProfilerInfoUnk->QueryInterface(__uuidof(ICorProfilerInfo8), reinterpret_cast(&this->corProfilerInfo));if (FAILED(queryInterfaceResult)){return E_FAIL;}// イベントマスクを設定して目的のイベントをフィルタリングします。DWORD eventMask = COR_PRF_MONITOR_ENTERLEAVE | COR_PRF_ENABLE_FUNCTION_ARGS | COR_PRF_ENABLE_FUNCTION_RETVAL | COR_PRF_ENABLE_FRAME_INFO;auto hr = this->corProfilerInfo->SetEventMask(eventMask);// 変数名の変更if (hr != S_OK){printf("ERROR: Profiler SetEventMask failed (HRESULT: %d)", hr);}// Enter, Leave, Tailcallのフック設定hr = this->corProfilerInfo->SetEnterLeaveFunctionHooks3WithInfo(EnterNaked, LeaveNaked, TailcallNaked);if (hr != S_OK){printf("ERROR: Profiler SetEnterLeaveFunctionHooks3WithInfo failed (HRESULT: %d)", hr);}return S_OK;}
最後に、関数の実行と終了を傍受するコードは次のとおりです。関数の実行時間とCall-Treeを作成するプロセスは、CallHistoryクラス内にカプセル化されています。
PROFILER_STUB EnterStub(FunctionID functionId, COR_PRF_ELT_INFO eltInfo){CallHistory::getInstance().Enter(functionId);}PROFILER_STUB LeaveStub(FunctionID functionId, COR_PRF_ELT_INFO eltInfo){CallHistory::getInstance().Leave(functionId);}PROFILER_STUB TailcallStub(FunctionID functionId, COR_PRF_ELT_INFO eltInfo){CallHistory::getInstance().Tailcall(functionId);}
以下のシーケンスダイアグラムは、ELTProfilerプロジェクトの主要な流れを図式化したものです。
① Enter:新しい関数が起動したときに発生するイベントです。
a. CallHistory オブジェクトに新しい関数が実行されたことを伝えます。CallHistoryはスタックを使用して関数呼び出しの深さを計算し、ツリー状の関数呼び出し履歴を出力できるようにします。
b. PerformanceCounter オブジェクトは、関数が進入して終了する時間を測定し、関数の実行にかかる時間を測定します。
c. MethodListオブジェクトはfunctionIdを介して関数名を取得します。一度インポートした関数名はハッシュマップに保存しておき、使用しながらパフォーマンスを最大限に引き上げます。
② Leave: 関数の終了時に発生するイベントです。
③ TailCall: Tailcall 状況で発生するイベントです。
a. Tailcallは、関数の終了時に別の関数を呼び出すときに発生する最適化された呼び出し方法です。これは、関数が返されたときにさらに処理するコードがなく、単に他の関数の結果値をそのまま返す場合に発生します。Tailcallが発生すると、現在の関数のスタックフレームを維持したまま他の関数を呼び出します。そのため、その関数が終了した後は元の関数に戻らず、元の関数の呼び出し元に直接返されます。この動作のため、callDepth は現在の値を保持します。言い換えれば、TailcallはcallDepthに追加の影響を与えません。
用途: 「Settings」クラスはアプリケーション設定値をロードして管理します。 環境変数を介して外部から設定を提供します。
用途:CallHistoryクラスは関数呼び出しの履歴を管理し、各関数呼び出しの開始と終了、およびその呼び出しの継続時間を記録します。このクラスを使用すると、プロファイリングデータに基づいて関数呼び出しツリーを視覚化できます。
用途:PerformanceCounterクラスはパフォーマンス測定のためのプロファイリングツールで、特定のコードの実行時間を測定します。
用途:MethodListクラスは、.NET関数の識別子(FunctionID)をその関数のフルネームにマッピングする役割を果たします。
用途:UdpSocketクラスは、UDPソケットを介してテキストデータを転送する機能を提供します。 プロファイリングデータまたはログメッセージをリモートシステムに送信するために使用できます。
コンソールで確認できない場合、またはリモートでログを確認したい場合は、udp-monitorプロジェクトをビルドして実行します。その後、run.batファイルを実行すると、すべてのメッセージが下の画像のようにUDPを介して転送されます。
「SetEnterLeaveFunctionHooks3WithInfo()」で指定される関数は「naked」と宣言されなければなりません。「naked」として宣言された関数は、コンパイラが生成するスタックフレームコード(prologとepilog)を含まない状態にコンパイルされます。 通常、関数が呼び出されたときにスタックフレームを設定してローカル変数やパラメータなどのスペースを確保し、関数終了時にこれを解放しますが、「naked」関数ではこれらの動作は自動的には行われません。
したがって、「naked」 関数を使うときは関数内部でスタックフレームの管理やレジスタの管理などを直接しなければなりません。これにより、スタックフレームまたはレジスタの状態を細かく制御できます。以下に示すコード例では、「EnterStub」関数が実行される前後に汎用レジスタの状態を保存して復元する作業が行われています。これは 「pushad」と「popad」 命令を使って行われます。
このようなアプローチは、.NET Profilerを作成する際に重要です。Profilerはコードの実行を妨げることなくコードの実行情報を収集する必要があるため、レジスタの状態やスタックフレームの状態を変更しないことが重要です。これを行うには、 naked 関数とアセンブリ命令を使用して、レジスタとスタックの状態を正確に制御します。
void __declspec(naked) EnterNaked(FunctionIDOrClientID functionIDOrClientID, COR_PRF_ELT_INFO eltInfo){__asm{PUSH EAXPUSH ECXPUSH EDXPUSH [ESP + 16]CALL EnterStubPOP EDXPOP ECXPOP EAXRET 8}}
テールコールは関数呼び出しの特別な形式です。 関数の最後で別の関数を呼び出すと、コンパイラまたはランタイムシステムは、現在の関数のスタックフレームを新しく呼び出された関数のスタックフレームにリサイクルできます。この最適化は主に再帰呼び出しで役に立ち、スタックオーバーフローを防ぎます。
再帰関数は自分自身を呼び出し続けるため、スタックにフレームが積み重ねられます。 したがって、深い再帰呼び出しを実行するとスタックオーバーフローのリスクがあります。 テールコール最適化を使用すると、これらの問題を回避できます。
すべての関数呼び出しがテールコールで最適化できるわけではありません。 テールコールで処理できる関数呼び出しは、次の条件を満たす必要があります。
int factorial(int n) {if (n <= 1) return 1;return n * factorial(n-1);}
上記の factorial 関数はテールコールで最適化できません。 なぜなら factorial(n-1) の結果に n を掛けて返すからです。ただし、次のようにコードを変更すると、テールコールで最適化できます。
int factorial_helper(int n, int acc) {if (n <= 1) return acc;return factorial_helper(n-1, n*acc);}int factorial(int n) {return factorial_helper(n, 1);}