|
2012-12-14[金] boost::container で俺俺アロケータこれは C++ Advent Calender 2012 14日目 の記事です。 C++11やboostをまだロクに使っていない人間なので、雰囲気を掴むために boost1.52 の boost/container フォルダを一寸覗いてみました。 new,delete等メモリーアロケート のコストを無視できないターゲットを扱うことも多いので、俺俺アロケータを渡してみたいってのもあり。 C++11仕様でアロケータが今までより楽にかけるようだし、scoped_allocator_adaptor も何か使えそうですし. ※いまだvc9をおもに使ってるので(vc12も使いますが)、C++11の記述はさけてます.
boost::containerboost::container ですが、C++11で仕様拡張された vector,list,map,set,deque 等のコンテナの機能を C++03コンパイラでもなるべく使えるようにした互換コンテナ群+α です。 C++11 で増えたコンテナ(array, unorderd_map 等)については すでにboostに実装があるためか(?) boost::container には含まれていません。詳しいことは こちら とか他のサイトにあたってください。 将来的にどんどん変わっていく部分も多々あるでしょうが、とりあえず 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/ 直下にあるファイルは
(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 の継承元になっています。 ということで 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 に依存しています.
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_;
};
かなりいい加減ですが、メモリーの雰囲気がわかれば...
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(実装面倒なのと、ターゲットであまり使わないし... いえ、時間切れ. 後日に何か)
stringstd::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 実装をベースにした特化版。
詳しいことは他のサイトにあたってください。
scoped_allocator_adaptor
C++11 で増えた アロケータです。
scoped_allocator_adaptor を用いれば、例えば map<string,int> のようなコンテナで、ローカルなアロケータをstringとmapで共通で使えるようにできます。 アロケータをコンテナのメンバーにするので、当然メモリー使用量も増えます(とくに内側のコンテナは数が多くなりやすく)。 ので、コンテナのメンバーにするアロケータはポインタだけにし、実装はさらにその先に用意、といった感じにすることになると思います。 詳しいことは こちら のサイトとかみてもらったほうがいいです。 正直なところ一見ではよくわからず、試してみてなんとなく納得の状態です。 以下、試してみたソース.
#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、単に(コピー)コンストラクタの頻度さげるだけじゃなくて、今回のような場合まさに必須の類でした。
おわりに
遅刻したうえに、
グダグダな内容ですみません。 例に挙げたソースのとおり allocator の記述に関してはすごく楽でした。 が、boost::contaneier を std:: にかえて vc12 でコンパイルすると全く×だったので boost::contaneier に結構依存していると思います。 時間の余裕なくなってるので、たぶん後日 補足修正書くことになりそうですが... おつきあいいただきありがとうございました。
追記: コンパイル試したときのソース |