私は、製品の変更によるパフォーマンス関連の影響を測定するためのツールを構築しています。
これを実現するために、関数が呼び出されるか戻るたびにトレースし、それについて通知するプロファイラーを実装しました。まず、出力をファイルにダンプして、使用するデータの感覚をつかみました。多かれ少なかれ、次のようになります。
FuncCall1
FuncCall2
FuncCall3
FuncRet3
FuncCall4
FuncRet4
FuncCall5
FuncCall6
FuncRet6
FuncRet5
FuncRet2
FuncRet1
このデータがどのように見えるかを視覚的に理解するために、最初の 10000 回の関数呼び出しのグラフを次に示します: (x 軸: 時間、y 軸: 深さ/ネスト):
( http://img444.imageshack.us /img444/4710/proflog.gif )
関数の実行が開始されると、その名前/識別子と現在の高精度のタイムスタンプがログに記録されます。関数が戻ると、開始時刻を保存したエントリを検索し、戻りをマークする新しいタイムスタンプを追加する必要があります。
要約すると、これらのデータに対して実行する操作は次のとおりです。
- 現在のタイムスタンプを持つ新しい関数呼び出しマークを挿入します。
- 特定の ID の最新の関数呼び出しを検索し、返されたタイムスタンプを保存します。
- 特定の関数内から呼び出された他の関数を確認します (そして、その時間が費やされている場所を確認します)。たとえば、前の例で関数 #2 を見ている場合、関数 #3、関数 # 4、Function#5 と Function#5 は Function#6 を呼び出し、それから戻ります (すべての呼び出し/戻りタイムスタンプがマークされています)。
さて、このシナリオに適したデータ構造のアイデアがいくつかあります。
各ノードのキーが関数識別子になり、各ノードの値がタイムスタンプ ペアのスタックになる自動バランス ツリー (AVL)。このアプローチにより、関数のタイムスタンプをマークするときに挿入と検索が高速になり、各ノードがスタックであるという事実が得られます。また、正しいリターン タイムスタンプを開始タイムスタンプに一致させることもできます。特定の関数は、最もネストされた/最近の関数呼び出しと一致する必要があります。このアプローチでは、ネストされた関数呼び出しを異なる識別子で維持するのは少し面倒です。なぜなら、ツリーを走査し、タイムスタンプに基づいて一致させてネストを把握する必要があるためです。理想的ではありません。
まだ返されていない関数のリストを維持し (呼び出しスタック情報を保持します)、各レベルが関数呼び出しの入れ子レベルと同じになるスキップ リストを使用します。このアプローチにより、操作 #3 が簡単になりますが、ルックアップは遅くなり、返されない関数 (main() など) の非常に長いリストを維持する必要が生じる可能性があります。ここでは、ハッシュテーブルを使用して、メモリ使用量を犠牲にしてルックアップ速度を向上させることもできます。メモリ使用量は重要です。このプロファイラーは約 20 MB/秒を簡単に生成します。
これらのデータを追跡するために単純なスタックを使用しない理由は、定期的に部分的な結果を別のマシンに同期し、すべてが返される前に少なくとも部分的な結果を利用できるようにする必要があるためです。
インターバル ツリー、レンジ ツリー、およびその他の種類のデータ構造を調べましたが、私の 3 つの要件すべてを効率的に満たすものは見つかりませんでした。
たぶん、私が気付いていないすべてを満たすデータ構造があるのでしょうか? 何か案は?
アップデート:
これはどうですか:
ネストされた呼び出しと一緒に関数呼び出しを持つツリーと、返されなかった関数用の別のスタックを持つ。
これで、スタック上のすべての要素がツリー内のコピーへのポインターを持ち、新しい関数呼び出しが到着すると、スタックの一番上の要素を見て、ツリー内の表現へのポインターをトレースし、新しい関数呼び出しを追加します。その呼び出しの子として、新しく作成されたツリー ノードへのポインターを使用して、そのコピーをスタックにプッシュします。
関数の戻り値の場合も同様です。関数の戻り値ごとに、スタックの最新のエントリは常にその呼び出しになります。呼び出しポインターをトレースし、戻り時間をツリーに保存し、呼び出しをポップします。
私の考えに大きな欠陥はありますか?
更新 2:
私のアプローチは完璧に機能しました。2 日間待って、質問にお答えします。