空想犬猫記

※当日記では、犬も猫も空想も扱っておりません。(旧・エト記)

第4のメモリ管理方法 〜 AutoReleasePool

C++では生ポインタなんか使わず,shared_ptr などのスマートポインタを使いなさいというのが,ごく一般的に言われることだと思う*1

JavaC#などのもう少し先進的な言語にのめり込んで,かつGCの効率の良さを主張する人々は,ガーベージコレクタ(ガベコレ)を使うべきだと言うかも知れない。

C++におけるメモリ管理の方法で,自分で delete を書くのが第1の方法だとしたら,第2,第3の方法はスマートポインタ,ガベコレを使う方法だろう。

shared_ptrのような参照カウンタ方式のスマートポインタの欠点は,相互参照が起こるケースで,お互いのオブジェクトがお互いの参照カウンタをインクリメントしあって,いつまでたっても開放されない,つまりメモリリークが発生することがあるということである。でまぁ,実際メモリリークは起こっていると思うけど,小さいうちは誰も気付かない。あるとき,明らかな桁違いの無駄遣いに気付いて,ようやく発覚する,というのが,過去の運用で体験したことだ(レベル低いかも知れないけど,現場はそんなもんだ)。

また,実際にスマートポインタを使ってみて分かったのは,既存のC++のライブラリは,リソースのマネージャがキッチリ定義されていて,敢えてスマートにせんでも,生ポインタで十分なように作られていて,逆にスマートポインタを混ぜると混乱が起こるということ。

というわけで,スマートポインタは既存のコードへの組み込みという観点でも,リソース管理の問題解決策という観点でも,非常に中途半端で,いまいちスッキリしないんである。前にも書いたかも知れないけど,私の一番のお気に入りのスマートポインタはauto_ptrで,その確信犯的な潔い使い勝手の悪さに,絶妙なバランス感覚を感じてしまう(参考)。で,ちなみにauto_ptrC++の中でも「使ってはいけない」リストのかなり上位に入っていると思う。嘆かわしい。

そういう意味では,潔くプログラマからリソース管理の責任を取り上げて,ガベコレを導入したほうが,プログラムを組む側としては余計なことを考えなくて済んで,非常にありがたい。ただ,実際にそこでガベコレという技術に乗っかれるような人は,そもそもC++なんか使ってないで,さっさとJavaC#を選ぶので,C++ではまだ主流ではないし,これからもなりにくいと思う。それにベターなCというポジションは絶対に外さないだろうから。

またC++に対する愚痴になりそうだけど,今となってはC++のターゲットとする分野って広すぎて,浅い。ローレベルなところではCに敵わないし(なにしろOSがCだからな),ハイレベルなところでは新手のオブジェクト指向言語に取って代わられつつある。ただ,未だに最速の(高級)オブジェクト指向言語の地位を保っている(と大勢の人に思われている)のが,せめてもの救い。

閑話休題。

話が明後日の方向に言ったところで,本題に戻る。

  1. C++ 使っておきながら,catch の中で delete とか,自分で書いたらやっぱ負けだよな
  2. auto_ptr 好きだけど,誰も使っちゃくれない。嘆かわしい。その他のスマートポインタは糞切れが悪いな
  3. ガベコレは,まぁ無理だろうな

と思って,さらに中途半端なメモリプールという技術をC++で実装してみた。スマートポインタを使う必要も無く,かつ自分でポインタを開放する必要も無い。

簡単な説明をすると,オーバーライトされたnewを使うと,Pool内にオブジェクトが作成され,Poolが破棄されるタイミングで,Pool内のオブジェクトが全て delete される。PoolからサブPoolを作成することもできる。Poolが破棄されるのは,自分自身かその親Poolが破棄されるとき。といった感じ。

コーディング規約がアレなのは,Windows向けに書いたものだから。で,もっと言うと,作ってみたものの,効率面と実装の簡潔さのバランス,あまりいい活用方法等が見つからなかったのでネットにリリースすることにしました。

元ネタはAPRのapr_pools.hで,所詮C++でごにょごにょやったところで,Cの簡潔さと実用性を遥かに凌ぐものが作れるかというと,そうでもないというのがよく分かる例になってしまったかもしれません。

C++で生計を立てながらも,ちょっくらC++に疲れている人のコードなので,変なところがあるかもしれません。腕に覚えのある人が,もっといいものを書いてくれることを期待します。

気になるところは色々ありますが,デストラクタが確実に呼ばれることを考えると,既存のコードでは,サブプールを持つ意味が,あまり無いかも知れないかなと思っています。

ソースコード

#include <vector>
#include <set>
#include <ctime>

struct MallocFree
{
   static void* Alloc(size_t nSize) { return malloc(nSize); }
   static void  Dealloc(void* ptr)  { free(ptr); }
};

template <typename T> struct Destructor
{
   static void Finalize(T* ptr)
   {
       ptr->T::~T();
   }
};

template
<
 typename T,
 typename FinalizerT = Destructor<T>,
 typename AllocatorT = MallocFree
>
class AutoReleasePool
{
public:
   typedef AllocatorT Allocator;
   typedef FinalizerT Finalizer;

public:
   class Buffer
   {
   public:
       Buffer() : m_bIdle(false)
       {}

       ~Buffer()
       {}

       void Sweep()
       {
           typename std::set<T*>::iterator it = m_setPool.begin();
           const typename std::set<T*>::iterator itEnd = m_setPool.end();
           for (; it != itEnd; ++it)
           {
               Finalizer::Finalize(*it);
               Allocator::Dealloc(*it);
           }
           m_setPool.clear();
       }

       void Invalidate()
       {
           m_setPool.clear();
           m_bIdle = true;
       }

       bool IsIdle() const
       {
           return m_bIdle;
       }

       void Register(T* pMember)
       {
           m_setPool.insert(pMember);
           m_bIdle = false;
       }

       void Unregister(T* pMember)
       {
           m_setPool.erase(pMember);
       }

   private:
       std::set<T*> m_setPool;
       bool m_bIdle;
   };


public:
   typedef std::vector<Buffer*> BufferList;

public:
   explicit AutoReleasePool(AutoReleasePool<T>* pParent = NULL)
       : m_pParent(pParent)
       , m_pBuffer(pParent ? pParent->BufferForSubPool() : new Buffer)
       , m_plstSubBuffer(NULL)
   {}

   ~AutoReleasePool()
   {
       Sweep();

       if (m_pParent)
           m_pParent->InvalidateBuffer(m_pBuffer);
       else
           delete m_pBuffer;

       m_pBuffer = NULL;

       if (m_plstSubBuffer)
       {
           typename BufferList::iterator itpSub = m_plstSubBuffer->begin();
           for (; itpSub != m_plstSubBuffer->end(); ++itpSub)
               delete *itpSub;
           delete m_plstSubBuffer;
           m_plstSubBuffer = NULL;
       }
   }

   void Sweep()
   {
       m_pBuffer->Sweep();
       if (m_plstSubBuffer)
       {
           typename BufferList::iterator itpSub = m_plstSubBuffer->begin();
           for (; itpSub != m_plstSubBuffer->end(); ++itpSub)
               (*itpSub)->Sweep();
       }
   }

private:
   template<typename U> friend void* operator new(size_t nSize, AutoReleasePool<U>& zPool);

   T* Register(T* pMember)
   {
       m_pBuffer->Register(pMember);
       return pMember;
   }

   void InvalidateBuffer(Buffer* pBuffer)
   {
       if (!m_plstSubBuffer)
           return;

       typename BufferList::iterator itpBuff = std::find(m_plstSubBuffer->begin(), m_plstSubBuffer->end(), pBuffer);

       if (itpBuff != m_plstSubBuffer->end())
           (*itpBuff)->Invalidate();
   }

   Buffer* BufferForSubPool()
   {
       if (!m_plstSubBuffer)
       {
           m_plstSubBuffer = new BufferList;
       }
       else
       {
           typename BufferList::iterator itpBuff = m_plstSubBuffer->begin();
           for (; itpBuff != m_plstSubBuffer->end(); ++itpBuff)
           {
               typename BufferList::value_type pBuff = *itpBuff;
               if (pBuff->IsIdle())
                   return pBuff;
           }
       }
       m_plstSubBuffer->push_back(new Buffer);
       return m_plstSubBuffer->back();
   }

   AutoReleasePool(const AutoReleasePool<T>&);
   const AutoReleasePool<T>& operator=(const AutoReleasePool<T>&);

private:
   AutoReleasePool<T>* const m_pParent;
   Buffer*          m_pBuffer;
   BufferList*      m_plstSubBuffer;
};

template <typename T> inline void* operator new(size_t nSize, AutoReleasePool<T>& zPool)
{
   void* ptr = AutoReleasePool<T>::Allocator::Alloc(nSize);
   zPool.Register(static_cast<T*>(ptr));
   return ptr;
}

サンプルコード

class A
{
public:
   static int s_nCount;
   A() : m_nID(++s_nCount) { std::cout << "Created(" << m_nID << ")\n"; }
   ~A() { std::cout << "Destroyed(" << m_nID << ")\n"; }
private:
   int m_nID;
};

class B : public A {};

int A::s_nCount = 0;

static void st_subfunc(AutoReleasePool<A>* pPool, bool bThrow)
{
   AutoReleasePool<A> zPool(pPool); //サブプール作成
   new(zPool) A;

   if (bThrow)
       throw 1;

   new(zPool) A;
   new(zPool) A;
}

int main()
{
   AutoReleasePool<A> zPool;

   new(zPool) A;
   new(zPool) B;

   std::cout << "call subfunc\n";
   st_subfunc(&zPool, false);
   std::cout << "end call subfunc\n";

   std::cout << "call subfunc\n";
   try
   {
       st_subfunc(&zPool, true);
   }
   catch (...)
   {}
   std::cout << "end call subfunc\n";

   return 0;
}

出力結果

Created(1)
Created(2)
call subfunc
Created(3)
Created(4)
Created(5)
Destroyed(3)
Destroyed(4)
Destroyed(5)
end call subfunc
call subfunc
Created(6)
Destroyed(6)
end call subfunc
Destroyed(1)
Destroyed(2)

*1:shared_ptr はマルチスレッドでもうまく動くように書かれているから,シングルスレッドでパフォーマンスを極度に気にするところでは,コンパイルオプションを工夫する必要があったりするらしい(まめ知識)