差分表示


#freeze
*&date(Y-n-j[lL],2012/12/14); boost::container で俺俺アロケータ

これは [[C++ Advent Calender 2012>http://partake.in/events/a02d7049-1473-4b69-b5ad-25ed416c5557]] 14日目 の記事です。

C++11やboostをまだロクに使っていない人間なので、雰囲気を掴むために boost1.52 の boost/container フォルダを一寸覗いてみました。
new,delete等メモリーアロケート のコストを無視できないターゲットを扱うことも多いので、俺俺アロケータを渡してみたいってのもあり。
C++11仕様でアロケータが今までより楽にかけるようだし、scoped_allocator_adaptor も何か使えそうですし.

※いまだvc9をおもに使ってるので(vc12も使いますが)、C++11の記述はさけてます.


** boost::container

boost::container ですが、C++11で仕様拡張された vector,list,map,set,deque 等のコンテナの機能を C++03コンパイラでもなるべく使えるようにした互換コンテナ群+α です。
C++11 で増えたコンテナ(array, unorderd_map 等)については すでにboostに実装があるためか(?) boost::container には含まれていません。詳しいことは
[[こちら>http://d.hatena.ne.jp/faith_and_brave/20110823/1314080258]]
とか他のサイトにあたってください。

将来的にどんどん変わっていく部分も多々あるでしょうが、とりあえず boost_1_52_0/boost/container/ フォルダで wc してみると、41ファイル 27613 行(detailフォルダ含)。
実際には container 外のboostヘッダをいろいろinclude しているので実質はもっと大きいです(map,set(tree)等は boost::intrusive のものを使っているようです)。
(ちなみに boost_1_52_0/boost/ では 8779ファイル 約172万行、やっぱりデカいです)

boost/container/ 直下にあるファイルは

|allocator_traits.hpp | std::allocator_traits 互換 |
|string.hpp| std::string 互換 |
|vector.hpp| std::vector 互換 |
|deque.hpp| std::deque 互換 |
|list.hpp| std::list 互換 |
|map.hpp| std::map・std::multimap 互換 |
|set.hpp| std::set・std::multiset 互換 |
|scoped_allocator.hpp| std::scoped_allocator 互換 |
|scoped_allocator_fwd.hpp| (scoped_allocator の最小宣言) |
|slist.hpp| 古のslist の拡張版 |
|stable_vector.hpp | vector<T*>で要素を別途アロケートのvector |
|flat_map.hpp| vector< pair<KEY,VALUE> > 風実装で mapインターフェースを持つコンテナ |
|flat_set.hpp| (上記の std::set 風実装) |
|container_fwd.hpp| boost::container |
|detail/ | 詳細実装のファイル群 |

(ptr_container のような)既存のstdコンテナを継承して拡張(メンバー関数追加)とかではないです。
(といっても boost::intrusive 等 boost内の他のライブラリはよく使われています)


次に、いくつかコンテナについて。

** vector

ざっくりメンバー変数の部分だけ抜き出してみて、

 template <class Allocator>
 class vector_alloc_holder {
     struct members_holder : public Allocator {
         pointer     m_start;
         size_type   m_size;
         size_type   m_capacity;
     }
 }
 template <class T, class Allocator>
 class vector : vector_alloc_holder<Allocator>;

(SGI版派生等)他の実装だと全てポインタで管理していることが多いですが、この実装はsize,capacityを個数で持っています(個人的にはデバッグ時にメンバー変数で見れるので好み)。

Allocatorは、(非static)メンバー変数がない場合に実質 0バイトになるよう struct の継承元になっています。~
stlの実装では 多少の差違はあれ Allocator は (非static)メンバー変数になっていることが多いですが、
C++03 の場合 static メンバー変数で持つのも有りだったようで、実際そうなっていたコンパイラもありました。~
C++11 では scoped_allocator_adaptor のこともあるし Allocator は (非static)メンバー変数必須のようです。

ということで C++11なら
 // 1回 N個 アロケートするだけの Allocator
 template<typename T, unsigned N>
 class SampleAllocator1 {
 public:
    typedef T value_type;
    SampleAllocator1() { }
    T* allocate(unsigned n) { assert(n == N); return reinterpret_cast<T*>(buf_); }
    void  deallocate(T*, unsigned) { }
    bool operator == (SampleAllocator1 const & ) const { return false; }
    bool operator != (SampleAllocator1 const & ) const { return true; }
 private:
    typedef double buf_t;
    buf_t   buf_[(N*sizeof(T)+sizeof(buf_t)-1) / sizeof(buf_t)];
 };
 //typedef std::vector<int, SampleAllocator1<int,1024> >  Int1024Vector;
 typedef boost::container::vector<int, SampleAllocator1<int,1024> >  Int1024Vector;
 Int1024Vector  g_intVec;

みたいな真似をしても大丈夫のはず...と書いたけど上記はたぶん boost::container に依存しています.~
(ターゲット環境で mallocの準備前に(固定サイズの)コンテナを使いたい...こともあった)


** list

実装は boost::intrusive の定義も多く面倒なので、かなり端折って (boost::container としてでなく) ありそうな list 実装のメンバーの雰囲気として以下
(すみません、後付で boost::container からめたはいいけど対処しきれず...)

 class Node_Base {
     Node_Base* next_;
     Node_Base* prev_;
 };
 template<typename T>
 class Node : Node_Base {
     T     value_;
 };
 template<typename T, class A >
 class List : A {
     ListNode_Base root_;
     size_t        size_;
 };

かなりいい加減ですが、メモリーの雰囲気がわかれば... ~

//(slist はメモリー的にはNodeのもつリンク(ポインタ)が1個のもの。)


** map,multimap, set, multiset

map,set は たいてい赤黒木 実装のようで、boost::container では boost::intrusive を使ってるようです。~
で、これも メモリーの雰囲気だけ...

 class Node_Base {
     Node_Base* left_;
     Node_Base* right_;
     Node_Base* parent_;
     bool       balance_;
 };
 template<typename T>
 class Node : Node_Base {
     T     value_;
 };
 template<typename T >
 class Tree : A {
     Node_Base root_;
     size_t    size_;
 };

~

List や Map は 渡された アロケータをそのまま使うのではなくて、allocator のメンバーの~
 template <class U> struct rebind { typedef allocator<U> other; };

を用いて、 T でなく Node<T> の allocator を使っています。
また、allocator への要求は通常 1個単位になると思います。

ということで、list や map の俺俺アロケータとしては、ノードサイズ固定のメモリーをプールしておく、というのが考えられます。

 template<unsigned B, unsigned N>
 class SampleAlloc2 {
 public:
     SampleAlloc2() {
         for (unsigned i = 0; i < N-1; ++i)
             buf_[i].link = &buf_[i+1];
         buf_[N-1].link = NULL;
         root_ = &buf_[0];
     }
     void* allocate(unsigned n) {
         assert(n == 1 && root_);
         Link*   p = root_;
         root_ = p->link;
         return p;
     }
     void  deallocate(void* t, unsigned n) {
         if (t) {
             assert(n == 1);
             Link*   p = reinterpret_cast<Link*>(t);
             p->link   = root_;
             root_     = p;
         }
     }
 private:
     union Link {
         Link*   link;
         char    b[B];
     };
 private:
     Link*       root_;
     Link        buf_[N];
 };
 enum { ELM_SIZE = 32 }; // コンテナのノードを含んだ要素のバイト数.
 enum { MAX_SIZE = 16 };
 template<typename T>
 class SampleAllocator2 : public SampleAlloc2<ELM_SIZE,MAX_SIZE> {
     typedef SampleAlloc2<ELM_SIZE,MAX_SIZE> base_type;
 public:
     typedef T value_type;
     SampleAllocator2() {BOOST_STATIC_ASSERT(sizeof(T) <= ELM_SIZE);}
     T* allocate(unsigned n) { return reinterpret_cast<T*>(base_type::allocate(n)); }
     void  deallocate(T* t, unsigned n) { base_type::deallocate(t, n); }
     bool operator == (SampleAllocator2 const & ) const { return false; }
     bool operator != (SampleAllocator2 const & ) const { return true; }
 };
 // list
 typedef SampleAllocator2<int>  IntAllocator;
 typedef boost::container::list<int, IntAllocator > IntList;
 // map
 struct cstr_less {
     bool operator()(const char* l, const char* r) const { return strcmp(l,r) < 0; }
 };
 typedef std::pair<char const* const,int> smpPair_t;
 typedef SampleAllocator2<smpPair_t> SmpPairAllocator;
 typedef boost::container::map<const char*, int, cstr_less, SmpPairAllocator >	StrIntMap;


※ 要素(ノード)サイズ(ELM_SIZE)が直うちでみっともないですが、とりあえずは。

** deque

(実装面倒なのと、ターゲットであまり使わないし... いえ、時間切れ. 後日に何か)


** string

std::stringは(C++03では)ライブラリごとに結構実装がばらけています(EffectiveSTL に載ってるのとか)。
c++11 で縛りがきつくなったので多少にかよってくるかもですが、それでもいろいろできそうです。

最小限としては vector と同様の内容になりそうですが...
boost::container::string から端折って抜き出してみると

  struct long_t {
     size_type      is_short  : 1;
     size_type      length    : (sizeof(size_type)*CHAR_BIT - 1);
     size_type      storage;
     pointer        start;
  };
  struct short_header {
     unsigned char  is_short  : 1;
     unsigned char  length    : (CHAR_BIT - 1);
  };
  struct short_t {
     short_header   h;
     value_type     data[UnalignedFinalInternalBufferChars]; 
                    // UnalignedFinalInternalBufferChars ≒ (sizeof(long_t)-sizeof(short_header))/sizeof(value_type)
  };
  union repr_t {
     long_raw_t  r;
     short_t     s;
  };

long_t は長い文字列でアロケータを用いる場合で、sizeof(long_t)-α 以内におさまる短い場合は short_t のようにして領域をバッファとして使い動的確保せずにすませる、という工夫がされています。
64bit CPUだと、vector互換でもsizeof(ポインタ)*3=24bytesになりますし、結構ありがたい実装です。
(最近作ってた 俺俺string がまさにこのタイプだった... もうboostのでよく)

※ メモ: ビットフィールドは エンディアン問わず、先に書かれたものが低アドレスに配置される.


** stable_vector, flat_map, flat_set

stl コンテナの代用品でなく、vector 実装をベースにした特化版。
~
stable_vector は、vector<T*> と要素を別途アロケートしたもの。(boost::ptr_vectorの類似品?)~
flat_map,flat_set は、vector<T> (mapはT=pair<KEY,VALUE>) にソート状態で登録して、インターフェースがmapやsetになったもの。~
各自で俺俺コンテナを作ってそうな類なので(ええ作ってました)、boost にあると楽になりそうです。
(できれば stable_vector を flat_map 化したものもあればうれしいところ)

詳しいことは他のサイトにあたってください。


** scoped_allocator_adaptor

C++11 で増えた アロケータです。~
//vector のところでも少し触れたとおり C++03 まではアロケータをstlコンテナのメンバーにできない実装のものがありえるため、stlコンテナに渡すアロケータは実質 staticインスタンスのみのものだけでしたが、C++11 ではコンテナのメンバーに持てるようになったようです。~

scoped_allocator_adaptor を用いれば、例えば map<string,int> のようなコンテナで、ローカルなアロケータをstringとmapで共通で使えるようにできます。~
※ 内側のコンテナ(この場合 string)のコンストラクタとして、デフォルトコンストラクタでなくアロケータを引数にとるコンストラクタが使われるようになります。

アロケータをコンテナのメンバーにするので、当然メモリー使用量も増えます(とくに内側のコンテナは数が多くなりやすく)。
ので、コンテナのメンバーにするアロケータはポインタだけにし、実装はさらにその先に用意、といった感じにすることになると思います。

詳しいことは [[こちら> http://d.hatena.ne.jp/faith_and_brave/20081128/1227867484]] のサイトとかみてもらったほうがいいです。

正直なところ一見ではよくわからず、試してみてなんとなく納得の状態です。

以下、試してみたソース.

 #include <stdio.h>
 #include <boost/container/map.hpp>
 #include <boost/container/string.hpp>
 #include <boost/container/scoped_allocator.hpp>
 #include <boost/foreach.hpp>
 
 // mapで解放しないこと前提に、スタックで解放無視の簡易アロケータ.
 template<unsigned N>
 class SampleAlloc3 {
 public:
     SampleAlloc3() : size_(0) { }
     void* allocate(unsigned n) {
         assert(size_+n <= N);
         void* p = ptr(size_);
         size_ += n;
         return p;
     }
     void  deallocate(void*, unsigned) { /*dummy*/ }
 private:
     typedef double buf_t;
     void*       ptr(unsigned n=0) { return (char*)buf_ + ((n + sizeof(buf_t)-1) & ~(sizeof(buf_t)-1)); }
 private:
     unsigned    size_;
     buf_t       buf_[N / sizeof(buf_t)];
 };
 
 // map<string,int> のメモリー
 template<unsigned SAMPLE_ALLOC3_SIZE>
 class Hoge {
 public:
     Hoge() : strIntMap_(std::less<String>(), MapAllocator(StrIntPairAlloc(&smpAlc3_))) {}
     void insert(const char* name, int val) {
         //strIntMap_[String(name, &smpAlc3_)] = val;
         strIntMap_.emplace( name, val );
     }
     void printAll() {
         BOOST_FOREACH(StrIntPair p , strIntMap_)
             printf("%s = %d\n", p.first.c_str(), p.second);
     }
 private:
     typedef SampleAlloc3<SAMPLE_ALLOC3_SIZE> Alloc;
     template<typename T>
     class SampleAllocator3 {
     public:
         typedef T value_type;
         // SampleAllocator3() : alc_(0) {}  // デフォルトコンストラクタは用意しないほうが安全.
         SampleAllocator3(Alloc* alc) : alc_(alc) { }
         template<typename U> SampleAllocator3(SampleAllocator3<U> const& r) : alc_(r.detail_get_ptr()) { }
         T* allocate(unsigned n) { return reinterpret_cast<T*>(alc_->allocate(n*sizeof(T))); }
         void  deallocate(T* p, unsigned n) { alc_->deallocate(p,n*sizeof(T)); }
         bool operator == (SampleAllocator3 const & ) const { return false; }
         bool operator != (SampleAllocator3 const & ) const { return true; }
         Alloc* detail_get_ptr() const { return alc_; }
     private:
         Alloc*      alc_;
     };
     typedef boost::container::basic_string<char, std::char_traits<char>, SampleAllocator3<char> >   String;
     typedef std::pair<const String, int>    StrIntPair;
     typedef SampleAllocator3<StrIntPair>    StrIntPairAlloc;
     typedef boost::container::scoped_allocator_adaptor< StrIntPairAlloc >           MapAllocator;
     typedef boost::container::map<String, int, std::less<String>, MapAllocator >    StrIntMap;
 private:
     SampleAlloc3<SAMPLE_ALLOC3_SIZE>    smpAlc3_;
     StrIntMap                           strIntMap_;
 };
 
 void sample3()
 {
     Hoge<1024>  hoge;
     hoge.insert("xyz", 1);
     hoge.insert("abcdefghijklmnopqrstuvwxyz", 2);
     hoge.insert("1234567890", 3);
     hoge.printAll();
 }

emplace, emplace_back、単に(コピー)コンストラクタの頻度さげるだけじゃなくて、今回のような場合まさに必須の類でした。~
共通で使うアロケータ(SampleAllocator3)には デフォルトコンストラクタをつけちゃダメ、というのを実感。~
( デフォルトコンストラクタつけてると insertや push_back 使ってもコンパイル通るため... ハマりました)


** おわりに

遅刻したうえに、
グダグダな内容ですみません。~
(当初の目論見からずれまくり...そもそも deque 調べるため boost::container 見始めたはずが)

例に挙げたソースのとおり allocator の記述に関してはすごく楽でした。
が、boost::contaneier を std:: にかえて vc12 でコンパイルすると全く×だったので
boost::contaneier に結構依存していると思います。

時間の余裕なくなってるので、たぶん後日 補足修正書くことになりそうですが...

おつきあいいただきありがとうございました。

~
[CR]
//明日 15日は (てもう今日)、@yohhoy さんです。
15日目は、@yohhoy さんです。

----
追記: [[コンパイル試したときのソース>http://www.6809.net/tenk/html/prog/BoostContainerSample.zip]]
----
#comment