5

Cで単純なマークアンドスイープガベージコレクターを実装しようとしています。アルゴリズムの最初のステップは、ルートを見つけることです。だから私の質問は、Cプログラムのルーツをどのように見つけることができるかということです。

mallocを使用するプログラムでは、カスタムアロケータを使用します。このカスタムアロケータは、Cプログラムから呼び出されるすべてのものであり、カスタムinit()の場合もあります。

ガベージコレクターは、プログラム内のすべてのポインター(ルート)が何であるかをどのように知るのですか?また、カスタムタイプのポインターが与えられた場合、その中のすべてのポインターをどのように取得しますか?

たとえば、クラスリストを指すポインタpがあり、その中に別のポインタがある場合は、qと言います。ガベージコレクターはそれをどのように認識し、マークを付けることができますか?

更新:初期化するときにすべてのポインター名とタイプをGCに送信するとどうなりますか?同様に、GCがツリーをトラバースできるように、さまざまなタイプの構造を送信することもできます。これは正気のアイデアでさえありますか、それとも私はただ夢中になっていますか?

4

2 に答える 2

13

まず第一に、C のガベージ コレクターは、広範なコンパイラーと OS のサポートがないため、保守的でなければなりません。正当なポインターと、たまたまポインターのように見える値を持つ整数とを区別できないからです。そして、保守的なガベージコレクターでさえ大変です実装する。のように、本当に難しい。また、許容できるものを得るために、言語を制約する必要がある場合がよくあります。たとえば、ポインターが隠されているか難読化されている場合、メモリを正しく収集することが不可能になる可能性があります。100 バイトを割り当てて、割り当ての 10 番目のバイトへのポインターのみを保持する場合、GC はブロックがまだ必要であることを認識できません。制御する必要があるもう 1 つの非常に重要な制約は、メモリのアライメントです。ポインタがアライメントされていないメモリ上にある可能性がある場合、コレクターは 10 倍以上遅くなる可能性があります。

ルートを見つけるには、スタックの開始位置とスタックの終了位置を知る必要があります。複数形に注意してください。各スレッドには独自のスタックがあり、目的によってはそれを考慮する必要がある場合があります。プラットフォーム固有の詳細を入力せずにスタックの開始場所を知るには (おそらくとにかく提供することはできません)、現在のスレッドのメイン関数内でアセンブリ コードを使用できます (スレッドmain化されていない実行可能ファイルのみ)。スタック レジスタをクエリします ( espx86 ではrsp、x86_64 ではこれら 2 つだけに名前を付けます)。Gcc と clang は、変数をレジスタに永続的に割り当てることができる言語拡張機能をサポートしているため、簡単に使用できます。

register void* stack asm("esp"); // replace esp with the name of your stack reg

(registerは標準言語のキーワードであり、今日のコンパイラではほとんどの場合無視されますが、 と組み合わせるとasm("register_name")、厄介なことを実行できます。)

重要なルートを忘れないようにするために、main関数の実際の作業を別のものに任せるべきです。(x86 プラットフォームでは、代わりにスタック フレームのベース ポインターであるebp/rbpをクエリしても、実際の作業はメイン関数で行うことができます。)

int main(int argc, const char** argv, const char** envp)
{
    register void* stack asm("esp");
    // put stack somewhere
    return do_main(argc, argv, envp);
}

コレクションを行うために GC に入ったら、中断したスレッドの現在のスタック ポインターを照会する必要があります。そのためには、設計固有および/またはプラットフォーム固有の呼び出しが必要になります (ただし、同じスレッドで何かを実行する場合、上記の手法は引き続き機能します)。

根の実際の狩りは今から始まります。朗報: ほとんどの ABI では、ポインターのサイズよりも大きな境界にスタック フレームを配置する必要があります。つまり、すべてのポインターが配置されたメモリ上にあると信頼できる場合は、スタック全体intptr_t*を内部は、マネージド ポインターのように見えます。

明らかに、他のルーツがあります。グローバル変数は (理論的には) ルートにすることができ、構造内のフィールドもルートにすることができます。レジスターは、オブジェクトへのポインターを持つこともできます。ルートになる可能性のあるグローバル変数を個別に説明する必要があります(または、私の意見ではそれを完全に禁止することは悪い考えではありません)。それらの自動検出は難しいためです(少なくとも、その方法はわかりません)任意のプラットフォームで)。

これらのルートはヒープ上の参照につながる可能性があり、注意しないとうまくいかない可能性があります。

すべてのプラットフォームがmallocイントロスペクションを提供しているわけではないため (私の知る限り)、スキャンされたメモリ、つまり GC が認識しているメモリの概念を実装する必要があります。少なくとも、そのような割り当てのそれぞれのアドレスとサイズを知る必要があります。これらのいずれかへの参照を取得すると、スタックに対して行ったのと同じように、ポインターをスキャンするだけです。(これは、ポインターが整列されていることに注意する必要があることを意味します。これは通常、コンパイラーにその仕事をさせる場合に当てはまりますが、サードパーティの API を使用する場合は注意が必要です)。

これは、GC が到達できない場所に収集可能なメモリへの参照を配置できないことも意味します。そして、これが最も痛い場所であり、特に注意する必要がある場所です. それ以外の場合、プラットフォームがmalloc イントロスペクションをサポートしている場合は、ポインターを取得する各割り当てのサイズを簡単に確認し、それらをオーバーランしないようにすることができます。

これは、トピックの表面をなぞっただけです。ガベージ コレクターは、シングル スレッドであっても非常に複雑です。スレッドをミックスに追加すると、まったく新しい傷の世界に入ります。

Apple は、このような保守的な GC を Objective-C 言語に実装し、libauto と名付けました。彼らは、Mac OS X の低レベル技術のかなりの部分と共に、それをオープンソース化しており、ソースはここで見つけることができます。

ここではホット・リックスしか引用できません: 頑張ってください!


さて、さらに先に進む前に、非常に重要なことを忘れていました。コンパイラの最適化によって GC が壊れる可能性があるということです。コンパイラがGCを認識していない場合、特定のルートをスタックに置くことはできず(レジスタで処理するだけです)、それらを見逃すことになります。レジスタを調べることができれば、これはシングルスレッド プログラムではそれほど問題にはなりませんが、マルチスレッド プログラムでは大きな問題になります。

また、割り当ての中断可能性についても十分に注意してください。新しいポインターを返している間、GC が開始されないようにする必要があります。これは、ルートに割り当てられる直前にそれを収集する可能性があり、プログラムが再開すると割り当てられるためです。あなたのプログラムへのその新しいダングリングポインター。

そして、ここに編集に対処するための更新があります:

更新: GC を初期化するときに、すべてのポインター名と型を GC に送信するとどうなりますか? 同様に、さまざまなタイプの構造体を送信して、GC がツリーをトラバースできるようにすることもできます。これは正気な考えですか、それとも私はただ頭がおかしくなっているのでしょうか?

メモリを割り当ててから、それを GC に登録して、それがマネージド リソースであることを伝えることができると思います。これにより、中断可能性の問題が解決されます。ただし、サードパーティのライブラリに送信するものには注意してください。サードパーティのライブラリへの参照が保持されていると、データ構造が GC に登録されないため、GC がそれを検出できない可能性があるためです。

そして、スタック上のルートではおそらくそれを行うことはできません。

于 2012-11-27T04:57:37.757 に答える
5

ルートは基本的にすべて静的および自動オブジェクトポインタです。静的ポインタは、ロードモジュール内でリンクされます。自動ポインタは、スタックフレームをスキャンして見つける必要があります。もちろん、スタックフレームのどこに自動ポインタがあるかはわかりません。

ルートを取得したら、オブジェクトをスキャンして、その中のすべてのポインタを見つける必要があります。(これにはポインタ配列が含まれます。)そのためには、クラスオブジェクトを識別し、そこからポインタの場所に関する情報を抽出する必要があります。もちろん、Cでは、多くのオブジェクトは仮想ではなく、その中にクラスポインタがありません。

幸運を!!

追加: 漠然とクエストを可能にする可能性のある1つの手法は、「保守的な」ガベージコレクションです。独自のアロケータを使用する予定なので、(何らかの方法で)割り当てのサイズと場所を追跡できるため、ストレージからポインタサイズのチャンクを選択して、「これは私のオブジェクトの1つへのポインタである可能性がありますか?」と尋ねることができます。もちろん、ランダムデータはオブジェクトの1つへのポインタのように見える可能性があるため、確実に知ることはできませんが、このメカニズムを使用して、ストレージのチャンク(コールスタックのフレームなど)をスキャンできます。または個々のオブジェクト)、アドレス指定する可能性のあるすべてのオブジェクトを識別します

保守的なコレクターでは、オブジェクトポインターのように見えるが実際には一部のアプリケーションにとって意味のあるデータである「ランダム」データを誤って変更する可能性があるため、オブジェクトの再配置/圧縮(オブジェクトを移動するときにオブジェクトへのポインターを変更する)を安全に行うことはできません。ただし、未使用のオブジェクトを識別し、それらが占めるスペースを解放して再利用することはできます。適切な設計により、非常に効果的な非コンパクトGCを実現できます。

(ただし、Cのバージョンでアラインされていないポインターが許可されている場合、バイトアラインメントのすべてのバリエーションを試す必要があるため、スキャンが非常に遅くなる可能性があります。)

于 2012-11-27T03:57:43.980 に答える