2

非常に .NET スタイルのアプローチを使用して、C++ 用のメモリ マネージャーを構築しています。その際、到達可能と見なされるオブジェクトを知る必要があります。到達可能なオブジェクトが問題のオブジェクトへのハンドルを持っている場合、オブジェクトは到達可能と見なされます。これは、どのオブジェクトが検索のルートであるかという問題を提起します。答えは、これらの「前夜」オブジェクトがスタック上にあるということです。それは、管理対象オブジェクトへのハンドルの形であろうと、それ自体が管理対象オブジェクトへのハンドルを持つスコープローカル オブジェクトのインスタンスであろうとです。

これに関するいくつかの記事を読み、Win32 API の StackWalk メソッドに関する MSDN の実装の詳細も確認しました。

いつものように、どんな助けも大歓迎です。また、メモリ マネージャーを作成しないようにアドバイスしたり、スマート ポインターなどの代替手段を提案したりしないでください。私は自分が何をしているのかを完全に理解しています。ありがとう!

4

1 に答える 1

3

あなたの要件は、私が現在取り組んでいる小さなプロジェクトに似ているように見えますが、私の目標はメモリマネージャーを作成することではなく、dmalloc (およびデバッグモードで長時間実行されるアプリケーション) を計測することです。定期的に実行を停止し、メモリをスキャンして、参照がないヒープ割り当てを探す機能を備えています。「ばかげた」ガベージ コレクターのようなものですが、メモリを解放することを目的としたものではありません。代わりに、後で分析するためにリークされた割り当てをログに記録することを目的としています (dmalloc に既に追加した、割り当て時にキャプチャされたスタック トレースと共に)。汎用メモリ マネージャーのガベージ コレクターとして、これはかなり非効率的なプロセスであり、実行に「長い」時間がかかることに注意してください (まだ完了していません。

いずれにせよ、メモリ マネージャーがアプリケーションのヒープ メモリの唯一のソースになると思いますか? また、システム内のスレッドは完全共有メモリ環境で動作し、他のスレッドからは見えないスタック スペースやスレッド ローカル ストレージ スペースなどのメモリを持たないスレッドはありませんか? もしそうなら...

ヒープ割り当てへのポインターを見つけることができるメモリのカテゴリは 4 つだけだと思います。

  1. 各スレッドのコールスタック
  2. ヒープ割り当て自体の中で
  3. 静的に割り当てられた書き込み可能なメモリ内 (.bss & .data/.sdata、.rdata/.rodata は除く)
  4. 各スレッドのスレッド ローカル ストレージ スペース内

ヒープ割り当てへのポインターがスタック上で発生する可能性があることは既に認識しています。割り当てへのポインターも可能性があります (代わりに可能性があります) ヒープ オブジェクト自体に格納され、スタックには格納されません。あなたの質問は、ガベージ コレクターの検索の「ルート」としてスタックを使用することを望んでいる可能性があることを示唆しています。これは、メモリ内のすべてのオブジェクトをトラバースし、すべての割り当てへのすべてのポインターが見つかるまで、メモリを介してあるオブジェクトから別のオブジェクトを検索し、スタック上のポインターを外側に向かって他の割り当てにたどることができることを望んでいることを意味しています。「ルート」ポインターは、静的に割り当てられたオブジェクトにも存在する可能性があり、スタック上にそのようなオブジェクトへのポインターがなくても直接参照できるため、すべての割り当てが「ポインター」から到達可能であると仮定することはできませんスタック。また、残念なことに、C++ では、各割り当ての構造を知ることができない限り (コンパイラの助けなしには理解できません)、どの場所もポインタである可能性があると想定する必要があります。したがって、メモリのこれら 4 つのカテゴリのそれぞれをスキャンして、既存のすべての割り当てへの潜在的なポインタを探す必要があります。割り当てのアドレスと一致するメモリ内の値が見つかった場合は、それぞれに「まだ使用中の可能性がある」というフラグを立てます。それが実際にポインタであるかどうか。メモリをスキャンすると、各バイト位置 (または、プラットフォームが位置ずれしたアドレスにポインターを持つことができないことがわかっている場合は、sizeof(void*) で割り切れる各バイト位置) で、割り当てのリストを検索する必要があります。その値が割り当てのリストにあるかどうかを確認します。実際にポインターであるかどうかに関係なく、割り当てのアドレスに一致する値がメモリ内に見つかった場合、それぞれに「まだ使用されている可能性がある」というフラグを立てます。メモリをスキャンすると、各バイト位置 (または、プラットフォームが位置ずれしたアドレスにポインターを持つことができないことがわかっている場合は、sizeof(void*) で割り切れる各バイト位置) で、割り当てのリストを検索する必要があります。その値が割り当てのリストにあるかどうかを確認します。実際にポインターであるかどうかに関係なく、割り当てのアドレスに一致する値がメモリ内に見つかった場合、それぞれに「まだ使用されている可能性がある」というフラグを立てます。メモリをスキャンすると、各バイト位置 (または、プラットフォームが位置ずれしたアドレスにポインターを持つことができないことがわかっている場合は、sizeof(void*) で割り切れる各バイト位置) で、割り当てのリストを検索する必要があります。その値が割り当てのリストにあるかどうかを確認します。

自分が何をしているのかを知っていると確信しているので、メモリマネージャーはおそらくこれらの割り当てをバランスの取れたツリー構造 (おそらく赤黒ツリーまたはアンダーソンツリー) で追跡しており、O(log n) の挿入と検索が可能です。しかし、これらのツリーをナビゲートするための比例定数は、ガベージ コレクターのパフォーマンスを実際に低下させます。ガベージ コレクション スキャンを実行する前に、ツリーの割り当てポインタをフラットな連続バッファ (つまり「配列」) に順番に (つまり、インオーダー トラバーサルを使用して昇順または降順で) コピーする必要があります。void*各割り当てのアドレスの配列と個別のビット配列をお勧めします (ないbool配列) 割り当てごとに 1 ビットで、すべてゼロに初期化されます。割り当ての対応するビットは、潜在的な参照が見つかった場合に 1 に設定されます。これにより、ガベージ コレクションのスキャン中に O(log n) ルックアップ (二分探索を使用) が得られますが、ルックアップの比例定数がはるかに管理しやすくなります。さらに、このよりコンパクトなデータ構造は、バランス ツリーよりもキャッシュ ヒットのパフォーマンスが優れている傾向があります。

次に、スキャンする必要があるメモリの 3 つのカテゴリについて説明します。

  • 各スレッドのコールスタック

このためには、スレッド マネージャーに各スレッドのスタックの上部と下部を照会できる必要があります。各スレッドの現在のスタック ポインタしか取得できない場合は、「バックトレース」API を使用して、そのスタックの関数リターン アドレスのリストを取得できる場合があります。そこから、各スタックのベース (わからない) に向かってスキャンし、最後のリターン アドレスに到達するまで順番に各リターン アドレスにチェックを入れることができます。 . また、「現在のスレッド」については、メモリ マネージャーに関連付けられたスタックフレームを含めないようにしてください。つまり、いくつかのスタックフレームをバックアップし、ガベージ コレクターに関連付けられているものを無視します。そうしないと、ガベージ コレクターのローカル変数にリークした割り当てのアドレスが見つかり、それらを

  • ヒープ割り当て自体の中で

ヒープ オブジェクトは相互に参照することができ、すべてが相互に参照するリーク オブジェクトのネットワークを持つことができますが、それらはグループとしてリークされます。お互いへのポインターを見たくないし、それらを「使用中」として扱いたくないので、これらを慎重に処理する必要があります...そして最後に。他のすべてのカテゴリが終了したら、void*割り当てアドレスのフラットな配列を折りたたむ/分割して、「使用中と見なされる」割り当てと「まだ検証されていない」割り当ての個別のリストを作成できます。「使用中と見なされる」割り当てをスキャンして、「まだ検証されていない」リストにある割り当てへの潜在的なポインタを探します。見つけたら、「未検証」リストから「使用中と見なされる」リストの最後に移動してください。

  • 静的に割り当てられた書き込み可能なメモリ内 (.bss & .data/.sdata、.rdata/.rodata は除く)

このためには、リンカーからこれらの各セクションの開始と終了 (または長さ) までのシンボルを取得する必要があります。そのようなシンボルがまだ存在しない場合、またはプラットフォーム API からその情報を取得できない場合は、リンカー コマンド スクリプト (リンカー スクリプト) を取得し、グローバル シンボルを開始アドレスに追加して初期化するように変更する必要があります。これらの各セクションのアドレス (または長さ)。.bss セクションには、初期化されていないグローバル、ファイル スコープ、およびクラスの静的データ メンバーが含まれています。.data/.sdata セクションには、非 const 事前初期化グローバル、ファイル スコープ、およびクラス静的データ メンバーが含まれます。プログラムは静的な const データにヒープ割り当てアドレスを書き込むことはないため、.rdata/.rodata セクションについて心配する必要はありません。

  • 各スレッドのスレッド ローカル ストレージ スペース内

このためには、各スレッドのスレッド ローカル ストレージ スペースをスレッド マネージャーにクエリできるようにする必要があります。そうしないと、各スレッドの起動の一部として、そのスレッド ローカル ストレージをスレッドのリストに追加する必要があります。アプリケーションのローカル スペースを削除し、スレッドの終了時に削除します。


まだ参加していて、これをやりたいと思っている場合は、最初に考えていたよりも大きなプロジェクトであることに気付いているでしょう。それがどうなるか教えてください!

于 2012-12-24T03:59:18.523 に答える