ガベージコレクターとC ++

C ++による手動メモリ管理は、同時に言語の最大のプラスとマイナスの1つです。 実際、このパラダイムにより、非常に生産的なプログラムを作成できますが、多くの問題も発生します。 それらを取り除くにはいくつかの方法があります。 いくつか試してみて、ガベージコレクターになりました。 この記事では、C ++でのガベージコレクターの実装ではなく、アイデアの歴史とその使用法、使用方法、最終的に放棄した理由について説明します。







だから、ほとんどのプログラマーと同じように、私自身のプロジェクト、つまり2次元のゲームエンジンを持っています。 すべての「実験」はその上で行われました。



ステージ1:メモリのデバッグ



手動メモリ管理で最も一般的な問題はリークです。 それらについて学ぶには、記憶に従う必要があります。 それがまさに私の最初のアプローチでした。 オブジェクトの作成と削除を追跡し、プログラムが完了した後、残っているものが削除されていないことを確認します。 これは非常に簡単です。new演算子とdelete演算子をオーバーロードして、ソースファイル名と割り当て元の行を受け入れ、すべての割り当てを1か所に保存できるようにします。 便宜上、ファイル名と行をステートメントに渡すマクロを宣言します。 したがって、オブジェクトを削除すると、対応する割り当てに関するレコードが削除されます。



コード
#include <vector> class MemoryManager { struct AllocInfo { const char* source; int line; int size; void* data; }; static MemoryManager instance; std::vector<AllocInfo> allocations; public: static void RegAllocation(void* ptr, int size, const char* source, int line) { AllocInfo info; info.data = ptr; info.size = size; info.source = source; info.line = line; instance.allocations.push_back(info); } static void UnregAllocation(void* ptr) { for (std::vector<AllocInfo>::iterator it = instance.allocations.begin(); it != instance.allocations.end(); ++it) { if (it->data == ptr) { instance.allocations.erase(it); return; } } } static void PrintInfo() { for (std::vector<AllocInfo>::iterator it = instance.allocations.begin(); it != instance.allocations.end(); ++it) printf("0x%x: %i bytes at %s: %i", it->data, it->size, it->source, it->line); } }; MemoryManager MemoryManager::instance; void* operator new(size_t size, const char* location, int line) { void* res = ::operator new(size); MemoryManager::RegAllocation(res, size, location, line); return res; } void operator delete(void* allocMemory, const char* location, int line) { MemoryManager::UnregAllocation(allocMemory); } void operator delete(void* allocMemory) { MemoryManager::UnregAllocation(allocMemory); } #define mnew new(__FILE__, __LINE__) int main() { int* data = mnew int; MemoryManager::PrintInfo(); return 0; }
      
      









長所





短所





ステージ2:スマートポインター



そして真実は、メモリ管理の問題に対する最も一般的な解決策はスマートポインタです。 インタビューでそれらについて尋ねられます。 アイデアは簡単です。通常のポインターの代わりに、ポインターのように機能するが、オブジェクトを自動的に解放する追加機能を備えたテンプレートクラスを使用します。 オブジェクトとともに、参照カウンターを格納します;ポインターに値を割り当てると、カウンターを増やします;ポインターを破壊すると、それを減らします。 カウンターがゼロの場合、誰もオブジェクトを必要とせず、解放できます。 ただし、小さな問題があります。2つのオブジェクトが相互に参照する場合、両方の参照カウントが1であるため、それらが解放されることはありません。







弱いポインターは、このループの問題を解決します。 ポインターの1つが弱くなり、参照カウントがゼロになり、両方のオブジェクトを解放できるようになりました。







さて、もっと複雑な状況を考えてみましょう。







この状況は弱い/強いポインターでも解決できますが、手動で制御する方が簡単ですか? プログラマが何か間違ったことをした場合、結果は同じになります:リークされた解放されていないオブジェクト。 実際、このような状況はまれであり、一般に、このアプローチはメモリを使用した作業を大幅に簡素化します。



長所





短所





ステージ3:サイクリング



スマートポインターを試したが、私は満足していなかった。 どこでも同じ種類のスマートポインターを使用し、ループリンクについては考えません。 彼にそれらについて考えさせてください。 しかし、それを行う方法は? 状況を想像してください:







下位の2つのオブジェクトはループされており、誰もそれらを参照していないため、削除できることを何らかの方法で見つける必要があります。 写真から推測するのはすでに簡単です。オブジェクトからのリンクが高レベルのオブジェクトにつながっていない場合は、解放できます。 上位レベルのオブジェクトは、大まかに言って、アプリケーションの初期化を開始するオブジェクトです。 C ++の場合、これらはスタック上のオブジェクトであり、静的です。



ステージ4:ガベージコレクター



注意深い読者は、これがガベージコレクターのスキームであり、その反対であることをすでに理解していると思います。 最も単純なコレクターは次のように機能します。トップレベルのオブジェクトからリンクをたどり、すべてのユーザーを関連性があるものとしてマークし、その後、マークされていないものを削除する権利があります。







実装には、スマートポインターの場合と同じテンプレートポインタークラスが必要です。 さらに、コレクター自身が必要です。コレクターはすべてを監視し、実際にゴミを収集します。



もう少しコード
 #include <vector> #include <map> #include <algorithm> class IPtr; template<typename T> struct Destroyer { static void Destroy(void* obj) { (*(T*)(obj)).~T(); } }; class MemoryManager { public: struct ObjectInfo { void* object; size_t size; bool mark; const char* source; int line; std::vector<IPtr*> pointers; void(*destroy)(void*) = nullptr; }; private: static MemoryManager instance; std::map<void*, ObjectInfo*> objects; std::vector<IPtr*> pointers; size_t allocatedBytes = 0; bool currentMark = true; public: static void CollectGarbage(); protected: void MarkObject(ObjectInfo* info); friend void* operator new(size_t size, const char* source, int line); friend void operator delete(void* object, const char* source, int line); friend void operator delete(void* object); template<typename T> friend class Ptr; }; MemoryManager MemoryManager::instance; class IPtr { protected: void* object; MemoryManager::ObjectInfo* info; public: virtual ~IPtr() {} virtual bool IsRoot() const = 0; protected: void MarkInvalid() { object = nullptr; info = nullptr; } friend void operator delete(void* object); friend class MemoryManager; }; template<typename T> class Ptr: public IPtr { public: Ptr() { object = nullptr; info = nullptr; MemoryManager::instance.pointers.push_back(this); } Ptr(T* object) { this->object = object; auto fnd = MemoryManager::instance.objects.find(object); if (fnd != MemoryManager::instance.objects.end()) { info = fnd->second; info->pointers.push_back(this); if (!info->destroy) info->destroy = &Destroyer<T>::Destroy; } MemoryManager::instance.pointers.push_back(this); } Ptr(const Ptr<T>& other) { object = other.object; info = other.info; if (info) info->pointers.push_back(this); MemoryManager::instance.pointers.push_back(this); } ~Ptr() { if (info) { auto fnd = std::find(info->pointers.begin(), info->pointers.end(), this); if (fnd != info->pointers.end()) info->pointers.erase(fnd); } auto fnd = std::find(MemoryManager::instance.pointers.begin(), MemoryManager::instance.pointers.end(), this); if (fnd != MemoryManager::instance.pointers.end()) MemoryManager::instance.pointers.erase(fnd); } T* Get() const { return (T*)object; } bool IsValid() const { return object != nullptr; } bool IsRoot() const { return false; } operator bool() { return object != nullptr; } operator T*() { return (T*)object; } T* operator->() { return (T*)object; } T& operator*() { return *(T*)object; } const T& operator*() const { return *(T*)object; } Ptr<T>& operator=(const Ptr<T>& other) { if (info) { auto fnd = std::find(info->pointers.begin(), info->pointers.end(), this); if (fnd != info->pointers.end()) info->pointers.erase(fnd); } object = other.object; info = other.info; if (info) info->pointers.push_back(this); return *this; } Ptr<T>& operator=(T* other) { if (info) { auto fnd = std::find(info->pointers.begin(), info->pointers.end(), this); if (fnd != info->pointers.end()) info->pointers.erase(fnd); } object = other; auto fnd = MemoryManager::instance.objects.find(object); if (fnd != MemoryManager::instance.objects.end()) { info = fnd->second; info->pointers.push_back(this); if (!info->destroy) info->destroy = &Destroyer<T>::Destroy; } else info = nullptr; return *this; } }; template<typename T> class RootPtr: public Ptr<T> { public: RootPtr():Ptr() {} RootPtr(T* object):Ptr(object) {} RootPtr(const Ptr<T>& other):Ptr(other) {} bool IsRoot() const { return true; } operator bool() { return object != nullptr; } operator T*() { return (T*)object; } T* operator->() { return (T*)object; } T& operator*() { return *(T*)object; } const T& operator*() const { return *(T*)object; } RootPtr<T>& operator=(const Ptr<T>& other) { Ptr<T>::operator=(other); return *this; } RootPtr<T>& operator=(T* other) { Ptr<T>::operator=(other); return *this; } }; void* operator new(size_t size, const char* source, int line) { void* res = malloc(size); MemoryManager::ObjectInfo* objInfo = new MemoryManager::ObjectInfo(); objInfo->object = res; objInfo->size = size; objInfo->mark = MemoryManager::instance.currentMark; objInfo->source = source; objInfo->line = line; MemoryManager::instance.objects[res] = objInfo; MemoryManager::instance.allocatedBytes += size; return res; } void operator delete(void* data, const char* source, int line) { delete data; } void operator delete(void* data) { auto fnd = MemoryManager::instance.objects.find(data); if (fnd != MemoryManager::instance.objects.end()) { MemoryManager::instance.allocatedBytes -= fnd->second->size; for (auto ptr : fnd->second->pointers) ptr->MarkInvalid(); delete fnd->second; MemoryManager::instance.objects.erase(fnd); } free(data); } void MemoryManager::CollectGarbage() { instance.currentMark = !instance.currentMark; for (auto ptr : instance.pointers) { if (ptr->IsRoot()) { if (ptr->info) instance.MarkObject(ptr->info); } } std::vector< std::map<void*, ObjectInfo*>::iterator > freeObjects; for (auto obj = instance.objects.begin(); obj != instance.objects.end(); ++obj) { if (obj->second->mark != instance.currentMark) freeObjects.push_back(obj); } for (auto obj : freeObjects) { instance.allocatedBytes -= obj->second->size; obj->second->destroy(obj->first); free(obj->first); for (auto ptr : obj->second->pointers) ptr->MarkInvalid(); delete obj->second; instance.objects.erase(obj); } } void MemoryManager::MarkObject(ObjectInfo* info) { info->mark = MemoryManager::instance.currentMark; char* left = (char*)info->object; char* right = left + info->size; for (auto ptr : instance.pointers) { char* cptr = (char*)ptr; if (cptr >= left && cptr < right) { if (ptr->info && ptr->info->mark != MemoryManager::instance.currentMark) MarkObject(ptr->info); } } } #define mnew new(__FILE__, __LINE__) struct B; struct C; struct D; struct A { Ptr<B> pb; Ptr<C> pc; A() { printf("A()\n"); } ~A() { printf("~A()\n"); } }; struct B { Ptr<C> pc; B() { printf("B()\n"); } ~B() { printf("~B()\n"); } }; struct C { Ptr<D> pd; C() { printf("C()\n"); } ~C() { printf("~C()\n"); } }; struct D { Ptr<C> pc; D() { printf("D()\n"); } ~D() { printf("~D()\n"); } }; int main() { RootPtr<A> pa = mnew A; pa->pb = mnew B; pa->pc = mnew C; pa->pc->pd = mnew D; pa->pc->pd->pc = pa->pc; pa->pc = nullptr; MemoryManager::CollectGarbage(); return 0; }
      
      









仕組み
作成されたオブジェクトごとに、 ObjectInfoメタ情報が作成され、MemoryManagerに保存されます。 そのような各オブジェクトは、オーバーロードされた演算子newによって作成されます。 ObjectInfoは、オブジェクトのサイズ、オブジェクトが作成された場所、オブジェクトへのポインタのリスト、およびこのオブジェクトのデストラクタを呼び出す関数へのポインタに関する情報を保存します。



使い慣れたポインターの代わりに、テンプレートクラスPtrおよびRootPtrを使用してオブジェクトを操作します。 RootPtrは 、最上位レベルのオブジェクトを示すために必要です。プログラムの実行中に、オブジェクトがスタック上または静的に作成されていることを見つけることができないためです(間違っている場合は修正してください)。 ポインターを初期化またはコピーすると、ポインターが追加され、対応するObjectInfoから削除されます。



ガベージコレクションが呼び出されると、マーカーのブール変数が反対に変わり、オブジェクトをマークするサイクルが始まります。 ループはトップレベルのポインターを通過し、次に子ポインターを再帰的に通過します。 子ポインターは、オブジェクトの本体にあるポインターです。 その後、マーカーが現在のオブジェクトと一致しないすべてのオブジェクトが削除されます。





気配りのある読者はもう1つ気づいたに違いありません。利便性のために、パフォーマンスで支払います。 ガベージコレクターの典型的な短所は、過剰なメモリ消費と長いガベージコレクションです。 ゲームエンジンの私のプロジェクトでは、これは致命的なマイナスです。なぜなら、すべてのフレームには数ミリ秒かかり、すべてを停止してゴミを収集する時間がないからです。 ただし、良い点があります。このガベージコレクターは完全に私たちのものであり、私たちは何でもできます!



ステージ5:即興演奏



最初に思い浮かぶのは、ガベージコレクションの瞬間を完全に制御することです。 ゲームプレイの途中でこれを行う必要はありません。 これは、レベルのロード中にオブジェクトの初期化と破壊の最大数が発生した場合にのみ実行できます。 ゲームを開発するとき、アクティブなゲーム中に多くの割り当て/殺害を行うことは慣習ではありません。 ショットごとに弾丸オブジェクトをロードして作成する場合、メモリの最適化によって節約されることはありません。 したがって、レベル間で長いガベージコレクションを行うことは、かなり粘り強いアイデアのように思えます。



次のアイデアは、ガベージコレクションをまったく行わないか、ごくまれに行うことです。 オブジェクトは依然として手動またはスマートポインターで削除され、ガベージコレクターはデバッガーとセーフティネットとして使用できます。 このオプションでは、ガベージコレクション中の手動メモリ管理とリークの安全性と同等のパフォーマンスが得られます。



このようにして、さまざまな方法でコレクターを使用できます。 ラピッドプロトタイピングでは、パフォーマンスについては心配しませんが、安定性が必要です。この場合、自動アセンブリを使用します。 通常の状況では、メモリを手動で管理しようとしますが、コレクターがそれを知らせてヘッジします。 そしてもちろん、それをオフにして完全に手動で制御することもできます。



さらに、さらにいくつかの「グッズ」を実装できます。 テンプレートポインタークラスを使用するため、それらが正しいかどうかを確認できます。つまり、オブジェクトを手動で削除する場合、そのオブジェクトへのすべての参照を無効にします。 そして、適切な場所でそれらを確認します。 別の可能な改善は、メモリの最適化です。 ガベージコレクションの段階で、メモリ内の実際のオブジェクトの位置を変更して、プロセッサキャッシュミスを減らすことができます。これにより、間違いなくパフォーマンスが向上します。



長所





短所





ステップ6:トップに戻る



最終的に、私のプロジェクトの詳細は、コレクターを拒否する決定に影響を与えました。 このプロジェクトはオープンソースコードで計画されており、主に使いやすさを目的としています。 既に複雑なC ++構文を特定のポインターで複雑にし、ガベージコレクターを追加すると、間違いなくプロジェクトに影響します。 開発者に新しいテクノロジーを紹介することを想像してください。特に、ほとんどのC ++プログラマーはすでに手動でメモリを管理しているため、彼は新しいAPIを学び、メモリ管理モデルを覚える必要があります。

スクリプトを使用することにしたとき、ようやく手動モデルに戻ったと確信しました。 シンプルさと利便性のために必要です。 プロトタイプまたは単純なゲームを行う-スクリプトを使用して、時間とリソースを節約します。 柔軟性とパフォーマンスが必要-C ++へようこそ。 C ++を使用する人は、ガベージコレクタを必要とする可能性はほとんどありません。



そして、私は最初に戻りました。 私の経験が他の自転車メーカーにとって有益であるか、少なくとも興味深いものになることを願っています。



PSこの記事のコードは最適化されておらず、作業を理解するためにのみ提供されています。



All Articles