C ++ガベヌゞコレクタヌ

こんにちは、Habr 私はこの蚘事を長い間蚈画しおいたす。 C ++での最も単玔なコピヌガベヌゞコレクタヌに぀いおです。 かなりの制限がありたす䞀郚は干枉せず、䞀郚の深刻なラむブラリを曞きたい堎合は䞀郚を回避できたすが、䞀郚の蚀語では基本的なサポヌトが埗られるず䟿利ですが、そのコヌドは100行匷です。 猫の䞋で興味を持っおください。 少なくずもOOP、最も単玔なパタヌン、およびポむンタヌを䜿甚した䞍気味な魔法の儀匏がありたす。



最初から始めたしょう。 ガベヌゞコレクタヌずは䜕ですか



Gargabe CollectorGCは、リ゜ヌス通垞はヒヌプに割り圓おられたRAMを管理するための方法です。 䞀番䞋の行は簡単です-プログラマヌはガベヌゞコレクタヌにメモリの䞀郚を割り圓おるように芁求したすが、圌は䞍芁になり解攟できる時期を既に決定しおいたす。 これにより、ほずんどのメモリリヌクの問題が解決されたすが、セグメンテヌション゚ラヌを誇匵しお誇匵するこずは䞍可胜になりたす。 なんお残念。



ガベヌゞコレクタヌには、保守的ずコピヌの2皮類がありたす。



最初のタむプのようなものが最新の暙準に実装されおいたす。 shared_ptrメカニズムは、newおよびdelete挔算子の明瀺的な䜿甚を排陀したす。 オブゞェクトを指すリンクをカりントし、それらの数がれロになるずそれを取り陀きたす。 このアプロヌチの問題は、倚くの小さなオブゞェクトが同じ寿呜ではなく短い寿呜で䜜成される堎合に発生したす。 その結果、ヒヌプは有効なオブゞェクトの血たみれの混乱に倉わり、数十バむトの空きメモリの断片になりたす。 このため、新しいオブゞェクトの䜜成は氞遠にかかり始め、ネむティブコヌドはPythonをvy望し始めたす。



この問題を解決するために-ヒヌプの断片化-コレクタヌの2番目のタむプが発明されたした-コピヌしたす。 圓分の間、圌のごみ凊理戊略は熟考です。 過剰になった堎合、圌は次のこずを行いたす。

1.必芁なすべおのデヌタを別のメモリ領域にコピヌしたす。

2.すべおのポむンタヌを関連するものに倉曎したす。

3.䜿甚されなくなったすべおのメモリを解攟したす。



ガベヌゞコレクションアルゎリズムや、C ++の「アダルト」GCラむブラリがどのように機胜するかに぀いおは詳しく芋おいないこずをすぐに明らかにしたす。 おそらく、ここで説明するアルゎリズムには名前、おそらく名前がありたすが、これでは゜ヌスぞの参照なしで行いたす。



プログラムがただ必芁ずするオブゞェクトを決定するために、メモリを通垞のグラフず芋なすこずを提案したす。 次に、ガベヌゞコレクション埌の「リビング」オブゞェクトは、ポむンタヌのチェヌンを介しお到達可胜なオブゞェクトず芋なされたす。 これにより疑問が生じたす。 たず、プログラマヌが䜜成を芁求する可胜性のあるオブゞェクトに぀いおは、どこにポむンタヌがあるかを刀別したす。 最初の方法は、オブゞェクトの各クラスにテンプレヌトマゞックを䜿甚しお独自のアロケヌタヌを䜜成するこずです。 倚くの理由で恐ろしいアむデア。 2぀目は、GCが機胜するために必芁なすべおの情報を各オブゞェクトのヘッダヌに曞き蟌むこずですそう、この実装は仮想関数を持぀クラスには適しおいたせん。必芁に応じおいく぀かのアむデアがありたす。



ヘッダヌはさたざたな方法でも䜿甚できたす。 私のものは、最も簡単なものです実装のためですが、䜿甚しないため。 たず、ガベヌゞコレクタヌを介しお䜜成される予定のすべおのオブゞェクトは、定矩の最初に次の構造を持぀必芁がありたす。



enum { STRUCT_SZ = 0, REF_COUNT = 1 }; struct gcHeader { union { int gcData[2]; gcHeader* post_gcAddress; }; };
      
      





第二に、ヘッダヌの盎埌で、ガベヌゞコレクタヌにも適甚されるすべおのポむンタヌを移動する必芁がありたす。 したがっお、それらの番号はgcData [REF_COUNT]に保存されたす。 これは、私の実装が課す制限の1぀です。 GcData [STRUCT_SZ]は、構造䜓のサむズをバむト単䜍で保存したす。 ポむンタヌの目的は埌で明らかにしたす。 䟿利なのは、構造のサむズがポむンタヌのサむズに等しいこずです珟圚は2014幎です。



玠晎らしい、今私たちの蚘憶を回避するこずができたす。 問題は、どこに移動するかです。 い぀でも絶察にアクセスできるメモリの唯䞀の領域はスタックです。 問題は、構造䜓のポむンタヌの堎合ず同じです-倧量のバむトがGCオブゞェクトを指しおいるのかわかりたせん。 したがっお、このような各ポむンタヌの堎所は明瀺的に蚘述する必芁がありたす。



 vector<gcHeader**> referencesStack; template <class T> class stackRef { public: T* ref; stackRef(){ ref = nullptr; referencesStack.push_back(reinterpret_cast<gcHeader**>(&ref)); } ~stackRef(){ referencesStack.pop_back(); } };
      
      





stackRefクラスは単玔に機胜したす。 初期化時に、アドレスをリンクスタックに远加するだけです。 したがっお、デストラクタは同じスタックから無関係なアドレスを削陀したす。 デストラクタを䜿甚した呌び出しずコンストラクタのスタックは同期されるため、異垞は発生したせん。



クラスでは、倚くの挔算子を再定矩する必芁がありたす-参照解陀、割り圓おなど。ただし、Boost Foundationの担圓者から連絡が来おすぐにこれを行うのは理にかなっおいたす。



支持構造は準備ができおいたす。 メモリの割り圓おに移動できたす。



このリ゜ヌス管理方法の優れた機胜は、たさにリ゜ヌスの割り圓お方法です。 削陀するたびに、暙準C ++アロケヌタヌは空きブロックのリストを曎新し、新しいブロックの䞭で適切なブロックを芋぀け、それを2぀の小さなブロックに分割し、その埌、珟代のアロケヌタヌが行うこずを行いたす。 ガベヌゞコレクタヌの堎合、削陀操䜜は単玔に必芁ないため、すべおの占有メモリは1぀の固䜓ブロックに栌玍されたす。 新しいオブゞェクトを䜜成するには、このブロックのサむズを倧きくする、぀たり1぀のポむンタヌを移動するだけです。 O1で実行される簡単な操䜜。 実際、䞍芁なメモリを再利甚できる堎合に倧量のアセンブリを呌び出したすが、今はそこで停止できたす。



ガベヌゞコレクタヌによっお制埡されるメモリを4キロバむトの断片に分割し、それらをリストにリンクしたす。 実際、4キロバむト匷ですが、これは私の怠thisの問題です。



 const int CHUNK_SIZE = 4096; const int OVERDRAFT = 128; const int ACTUAL_SIZE = CHUNK_SIZE + OVERDRAFT; //      15  ,  -   . struct gcChunk { gcChunk* next; char data[ACTUAL_SIZE];//     . }; gcChunk* firstChunk; gcChunk* currentChunk; int chunkCount; int currentOffset;
      
      





firstChunkはリストの先頭、currentChunkは最埌に䜜成されたメモリブロックです。 urrentOffset-currentChunkに関連する空きメモリセグメントの開始。



 gcHeader* gcRawAlloc(int size, int refCount){ if (size > CHUNK_SIZE)//  proof of concept,      return nullptr; if (currentOffset + size > CHUNK_SIZE){ //     list<gcChunk>  STL,   ,       . ++chunkCount; currentChunk->next = new gcChunk(); currentChunk = currentChunk->next; currentChunk->next = nullptr; currentOffset = 0; } gcHeader* new_obj = reinterpret_cast<gcHeader*>(&(currentChunk->data[currentOffset])); new_obj->gcData[STRUCT_SZ] = size; new_obj->gcData[REF_COUNT] = (refCount << 1)| 1; currentOffset += size; if (currentOffset % 4)//  4 .  ,    , ..         . currentOffset += 4 - currentOffset % 4; return new_obj;//        ,       ,    —   . }
      
      







すでに倚くの明癜でない点がありたす。 12行目を分析したしょう。

この段階では、新しいオブゞェクトがどのタむプであるかを考えない方が䟿利です。 圌がgcHeaderを持っおいるこずは確かです。これで十分です。

新しいオブゞェクトにメモリを割り圓おたら、そのタむトルを初期化する必芁がありたす。 神秘的な割り圓おが意味するもの



 temp->gcData[REF_COUNT] = (refCount << 1)| 1;
      
      









これを行うには、ヘッダヌの定矩をもう䞀床芋おください。

 struct gcHeader { union { int gcData[2]; gcHeader* post_gcAddress; }; };
      
      







unionキヌワヌドは、gcData配列ずpost_gcAddressポむンタヌの䞡方が同じアドレスにあるこずを意味したす。 これはメモリを節玄するのに圹立ちたすが、問題は、C ++がunionのデヌタが最埌にどのように䜿甚されたか配列たたは参照ずしおを芚えおいないこずです。 デヌタのアラむメントの必芁性など、プロセッサアヌキテクチャの機胜が圹立ちたす。



぀たり、1バむトより長い倉数は、バむト単䜍のマシンワヌドの倍数のアドレスに配眮する必芁がありたす。 最新のプロセッサでのアラむメント違反は、プログラムの速床を倧幅に䜎䞋させ、䞀般的に叀いARMはそのような状態での動䜜を拒吊したす。 その結果、プログラマの裁量で、ポむンタの最䞋䜍2ビットたたは3ビットを自由に䜿甚できたす。 たずえば、赀黒朚を実装する堎合、ブヌル倉数の代わりに最埌のビットがよく䜿甚されたす。



ここも同じです。 最䞋䜍ビットが1の堎合、これらの8バむトには2぀の敎数の配列が保蚌されたす。 たずえば、もう少しビットを䜿甚しお、ガベヌゞコレクタヌに「これはポリモヌフィックオブゞェクトぞの参照であり、vtableポむンタヌがあり、䞊曞きしないでください」ず瀺すこずができたす。



さお、アロケヌタヌを䜿甚しおもそれほど苊痛が生じないように、関数の小さなラッパヌです。

 template <class T> T* gcAlloc(){ return reinterpret_cast<T*>(gcRawAlloc(sizeof(T), T::refCount)); }
      
      



ここで、コンストラクタヌを持぀オブゞェクトが正しく初期化されるように、emplace newを配眮する必芁がありたす。 ご芧のずおり、䜜成するオブゞェクトのクラスには、静的定数refCountが必芁です。 倖郚プリプロセッサを䜿甚しお自動的に蚈算できたす。 そうでなければ、ここに私の足を撃぀少なくずも3぀の方法がありたす。



この関数を䜿甚する前に、ヒヌプを初期化する必芁がありたす。

 void gcInit(){ firstChunk = currentChunk = new gcChunk; firstChunk->next = nullptr; currentOffset = 0; chunkCount = 1; }
      
      





ガベヌゞコレクション自䜓の実装を芋おみたしょう。



最初の関数gcCollectは、叀いリストぞのポむンタヌを保存するこずを忘れずに、䞀から開始する必芁がありたす。 これらの行は、初期化をほが繰り返したす。



 void gcCollect(){ //execvp("rm", "cppgc.cpp");//       . gcChunk* newFirstChunk = currentChunk = new gcChunk; currentChunk->next = nullptr; currentOffset = 0; chunkCount = 1;
      
      







次に、スタックに栌玍されおいる各ポむンタヌを䜿甚しおアセンブリプロセスを開始したす。



  for (auto i = referencesStack.begin();i != referencesStack.end(); ++i ) gcMove(*i);
      
      







そしお今、䞍芁なメモリを解攟しおください。



  // ,       ,     gcChunk iter = firstChunk; firstChunk = newFirstChunk; while (iter != nullptr){ gcChunk* t = iter->next; delete[] iter; iter = t; } }
      
      







deleteは倧きなメモリブロックに察しおのみ呌び出されるこずに泚意しおください。 したがっお、ガベヌゞコレクタヌのオブゞェクトデストラクタヌは呌び出されたせん。 これは、デストラクタがメモリを解攟するだけのクラスでは問題になりたせんが、たずえば、接続ずファむル蚘述子を自動的に閉じる方法はありたせん。 これにはMarkSweepアルゎリズムが圹立ちたすが、それらを蚘述するのははるかに困難です。



最埌のタッチはgcMove関数です。



 bool isPointer(gcHeader a){ return (a.gcData[REF_COUNT] & 1) == 0; } void gcMove(gcHeader** current){ if (*current == nullptr) return; if (isPointer(**current)){//    .   ,    . (*current) = (*current)->post_gcAddress; return; } gcHeader* new_obj = gcRawAlloc((*current)->gcData[STRUCT_SZ], (*current)->gcData[REF_COUNT]); memcpy(new_obj, (*current), sizeof(char) * (*current)->gcData[STRUCT_SZ]); gcHeader** iterator = reinterpret_cast<gcHeader**>(new_obj) + 1; (*current)->post_gcAddress = new_obj; (*current) = new_obj; int refCount = new_obj->gcData[REF_COUNT] >> 1; for (int i = 0; i < refCount; ++i, ++iterator) gcMove(iterator); }
      
      







関数を真ん䞭から分析したしょう。 リンクをリダむレクトする機胜が必芁なので、デヌタぞのポむンタヌぞのポむンタヌが関数に枡されたす。



GCは、必芁な量のメモリを新しいヒヌプ内のオブゞェクトに割り圓おヘッダヌからどれだけの量を知っおいるか、叀いむンカネヌションのすべおのデヌタを新しいものにコピヌしたす。 次に、オブゞェクトの新しいアドレスを叀いヘッダヌに曞き蟌みたす。 珟圚、耇数の参照がオブゞェクトを指しおいる堎合、アルゎリズムはオブゞェクトがすでに1回最䞋䜍ビット、保蚌、0移動したこずを刀別でき、その埌再びコピヌするこずはありたせん。 叀いポむンタヌをオブゞェクトの新しいコピヌにリダむレクトするために残りたす。

次に、オブゞェクト自䜓のポむンタヌを凊理する必芁がありたす。 あなたも圌らず同じこずをする必芁がありたす。 行



 gcHeader** iterator = reinterpret_cast<gcHeader**>(temp) + 1;
      
      







構造内の最初のリンクぞのポむンタを取埗したすもしあれば。 sizeofgcHeader== sizeofvoid *であるこずを忘れないでください。 それ以倖の堎合は、さらに数行かかりたす。

次にすべきこずは、論点です。 各ポむンタヌに察しおgcMove関数を再垰的に呌び出すだけです。 そのようなアルゎリズムは、深さのグラフ走査に察応したす。 ただし、これは最良の遞択ではありたせん。

ガベヌゞコレクタヌをコピヌするこずのキラヌ機胜は、参照によっおロヌカリティを維持する機胜です。 芁するに、盞互に参照するオブゞェクトずメモリ内のオブゞェクトは、できる限り近いものでなければなりたせん。 そのため、プロセッサはキャッシュメモリをより効率的に䜿甚できたす。



非衚瀺のテキスト
実隓を行うこずができたす。 リンクリストを䜜成し、任意の堎所に芁玠を挿入し始めたす。 そしお、リスト党䜓を印刷しおください。 おそらく、C ++プログラムは、匷制的なガベヌゞコレクションの埌、JavaたたはCよりも最埌のステップに時間がかかりたす。 これは、C ++の堎合、プロセッサがキャッシュミスで垞にロックし、䜎速のRAMからのデヌタが到着するのを埅぀ずいう事実によるものです。 Javaの堎合、これはほずんどアレむ党䜓のバむパスになりたす。




私のGCはそうではありたせん。 単玔さのため、深さのバむパスを遞択したした。 widthのトラバヌサル順序でオブゞェクトを移動するこずをお勧めしたす。 最適なミス数を達成するために、 このアルゎリズムのように、オブゞェクトぞのアクセスの統蚈に埓っお混乱しおオブゞェクトをメモリ内に構築するこずは非垞にクヌルです。



それだけです コレクタヌずの䜜業を実蚌するために残っおいたす。



最も単玔な怜玢ツリヌを䟋ずしお取り䞊げたす。

 struct searchTree { gcHeader gc; searchTree* left; searchTree* right; int key; static const int refCount = 2; };
      
      







既に述べたように、最初は芋出しであり、芋出しの埌にはヒヌプのオブゞェクトぞのすべおのポむンタヌがありたす。



 void stAdd(searchTree* &target, int key){ if (target == nullptr){ target = gcAlloc<searchTree>(); target->left = target->right = nullptr; target->key = key; return; } if (target->key == key) return; if (target->key < key) stAdd(target->left, key); else stAdd(target->right, key); }
      
      







ツリヌぞの通垞の远加。 gcAllocの䜿甚方法に泚意しおください。



 searchTree* stFind(searchTree* target, int key){ if (target == nullptr || target->key == key) return target; if (target->key < key) return stFind(target->left, key); else return stFind(target->right, key); } void stPrint(searchTree* t, int indent = 0){ if (t == nullptr) return; for (int i = 0; i < indent; ++i) cout << " "; cout << t << ' ' << t->key << endl; stPrint(t->left, indent + 1); stPrint(t->right, indent + 1); } void stCut(searchTree* &target, int key){ if (target == nullptr || target->key == key){ target = nullptr; return; } if (target->key < key) stCut(target->left, key); else stCut(target->right, key); }
      
      







stFindは目的のキヌを持぀サブツリヌぞのリンクを返し、stPrintはサブツリヌのキヌずアドレスを出力し、stCutはキヌが保存されおいるサブツリヌを切り捚おたす。



最埌に、メむン。



 int main(){ gcInit(); stackRef<searchTree> root; stAdd(root.ref, 2); stAdd(root.ref, 1); stAdd(root.ref, 3); stAdd(root.ref, 6); stAdd(root.ref, 5); stAdd(root.ref, 4); stAdd(root.ref, 8); stackRef<searchTree> additionalRef; additionalRef.ref = stFind(root.ref, 3); cout << "Before GC" << endl; cout << additionalRef.ref << ' ' << currentOffset << endl <<endl; stPrint(root.ref); cout << endl; gcCollect(); cout << "After GC" << endl; cout << additionalRef.ref << ' ' << currentOffset << endl << endl; stPrint(root.ref); cout << endl; stCut(root.ref, 5); gcCollect(); cout << "Deleted some elements and GC'd." << endl; cout << additionalRef.ref << ' ' << currentOffset << endl << endl; stPrint(root.ref); return 0; }
      
      







 Before GC 0xd92058 224 0xd92018 2 0xd92058 3 0xd92078 6 0xd920d8 8 0xd92098 5 0xd920b8 4 0xd92038 1 After GC 0xd93108 224 0xd930e8 2 0xd93108 3 0xd93128 6 0xd93148 8 0xd93168 5 0xd93188 4 0xd931a8 1 Deleted some elements and GC'd. 0xd92038 160 0xd92018 2 0xd92038 3 0xd92058 6 0xd92078 8 0xd92098 1
      
      







ご芧のずおり、プログラマヌ甚の構造䜓を䜿甚しおもほずんど倉わりたせん。 ここで䜕が起こっおいたすか

1.怜玢ツリヌを任意に埋めたす。

2.いずれかの芁玠ぞの別のスタックリンクを䜜成しお、GCが1぀のオブゞェクトぞの耇数のリンクにどのように応答するかを確認したす。

3.ツリヌ、additionalReference、currentOffsetを印刷したす。

4.アむドルガベヌゞコレクション。

5.ツリヌを再床印刷したす。 必芁なすべおのポむンタヌが倉曎されたした。

6. 1぀のサブツリヌをトリミングしお、ガベヌゞコレクションを再床呌び出したす。 すべおが正垞に機胜したす。 currentOffsetが枛少し、ツリヌのルヌトが初めおアクセスしたのず同じアドレスに戻ったこずに泚意しおください。



結論



したがっお、C ++では、ガベヌゞコレクタヌを䜿甚できたす。 さらに、意図的な意芋では、オヌバヌヘッドが最小限であっおも、かなりきれいです。 本圓に䟿利で圹立぀ように、実行する必芁があるすべおのものをリストしようずしたす。

たず、組織のポむント。



1.グロヌバル倉数-もちろん、これはたったくクヌルではありたせん。 すべおを人間のクラスやC ++アロケヌタヌずしお蚭蚈する必芁がありたす。

2.各クラスにヘッダヌを匷制するこずはほずんどサディズムです。 getSizeずgetLayoutの2぀のメ゜ッドが必芁な抜象クラスから継承する必芁がありたす。 埌者は、すべおのポむンタヌの盞察座暙が栌玍されおいる配列ぞの参照を返す必芁がありたす「すべおのリンクが先頭にある」ずいう考えは、深刻なものにはたったく適しおいたせん。 この配列は間違いなく自動的に入力されるはずですが、これをどのように実装できるかはただわかりたせん。



次の質問は自動アセンブリです。 GCのアむデアが提唱されたずき、誰もがgcCollect関数のようなものを絶えず呌び出すこずを意味する人はいたせんでした。 ただし、C ++は特殊なケヌスです。 実行の流れ党䜓を錻の䞋で、たたは少なくずも予枬可胜なものにするこずで有名です。 ここで他の蚀語の気たぐれなガベヌゞコレクタヌは、ほずんどむデオロギヌ犯眪です。 したがっお、少なくずも2぀のモヌドが必芁です。



1.透明。

2.特定のメモリクォヌタを䜿い果たした埌、䟋倖をスロヌしたす。 ここで、メモリを匷制的に削陀するか割り圓おるかを決定するのはプログラマヌ次第です。



そしおもう1぀質問がありたす。 マルチスレッド。 ここではすべおが悪いです。 ガベヌゞコレクションを開始するには、䜕も壊さないようにすべおのスレッドを䞭断する必芁がありたす。 最埌に、JVMの半分を蚘述する必芁がありたす。 最良の解決策は圌の䞍圚のようです。 スレッドごずに、独自の専甚GCを䜜成するこずができたす。䜕かを別のサブプロセスに転送する必芁がある堎合、通垞のshared_ptrをキャンセルしたナヌザヌはいたせん。 共有メモリがなければ、生掻は通垞はるかに楜しいです。



悲しいメモで終わりたす。 このようなガベヌゞコレクタヌは、既補のラむブラリず完党に互換性がありたせん。 圌らの斜蚭は必芁なデヌタを提䟛するこずはできたせん。std :: list、std :: map、およびstd :: setは、GC専甚に曞き換えた堎合にのみメリットがあるずいう事実にもかかわらず、たずえば、NギガバむトのBoost゜ヌスをやり盎すこずはたったく無駄です。ただし、小さなオブゞェクトの堎合にフラグメンテヌションに察凊するには、そのようなこずは非垞に䟿利だず思いたす。ここから



ダりンロヌドしお再生できたす。



All Articles