最終更新:2026-10-12
STL 空基底クラス最適化 EBO¶
0.はじめに¶
EBOはEmpty Base Optimizationの略称です。
本節では、空クラスから始めて、STL内部、テスト、そして自分でEBOを実装して性能を比較し、最後にもう一度テストしてまとめます。
1.空クラス¶
空クラスを定義します:メンバ変数がなく、継承がなく、データ要素がないクラスです。
空なので、このsizeofはいくつでしょうか?
結果は1です。空なのにどうして0ではないのでしょうか?
空クラスも同様にインスタンス化でき、各インスタンスはメモリ上に一意のアドレスを持ちます。これを実現するために、コンパイラは空クラスに暗黙的に1バイトを付与することが多く、これにより空クラスはインスタンス化後にメモリ上で一意のアドレスを得られます。したがって、上記のサイズは1です。
上の答えから、皆さんはおそらく別の疑問を持つでしょう:なぜ2つの異なるオブジェクトのアドレスは異なるべきなのでしょうか?
次の例があります:
class Empty{
public:
void print() {
std::cout<<"I am Empty class"<<std::endl;
}
};
template < typename T >
bool isSame( T const & t1, T const & t2 )
{
return &t1 == &t2;
}
テストしてみましょう:
int main() {
Empty a,b;
assert(!isSame(a,b)); // 编译通过,a与b的地址不同
Empty *p=new Empty;
Empty *q=new Empty;
assert(!isSame(p,q)); // 编译通过,a与b的地址不同
return 0;
}
上のテストで、2つの異なるオブジェクトのアドレスは異なることが確認できました。次のシーンを考えてみます:
このとき、もしaのアドレスとbのアドレスが同じだったら、同じアドレスに2つのオブジェクトが存在することになり、ポインタでそれらを参照するときに2つのオブジェクトを区別できなくなってしまうと気づきます。したがって、2つの異なるオブジェクトのアドレスは異なるのです。
2.空基底クラス最適化¶
さて、次の2つの使い方を比較します。1つ目は、あるクラスの中にもう1つのクラスをメンバとして含み、それを通して包含されるクラスの機能を得る方法です。
もう1つは、継承を直接使って基底クラスのメンバ関数やその他の機能などを得る方法です。
次にテストをします:
出力:
1つ目の方法では、バイトアライメントのために、本来1バイトしか占めなかったものが4の倍数へと拡張され、最終的に8バイトになります。
この2つを比較すると、2つ目の継承によって基底クラスの機能を得る方法は、余分なサイズが発生しないという最適化になっており、これをEBO(空基底クラス最適化)と呼びます。
それでは、STLのソースコードに戻り、その中での使用を見てみましょう!
3.STLにおけるEBOの世界¶
deque、rb_tree、listなどのコンテナは、いずれもメモリ管理と切り離せません。これらのコンテナにはそれぞれ対応するメモリ管理が含まれており、_xx_implを通して次のいくつかのクラスを継承しています:
std::allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::__mt_alloc<_Tp>
__gnu_cxx::__pool_alloc<_Tp>
__gnu_cxx::malloc_allocator<_Tp>
これは私たちのEBOと何の関係があるのでしょうか?
実は、上に挙げた継承の基底クラスはいずれもメモリ管理のEBO(空基底クラス)です。
各コンテナ内での使用では、それぞれのメモリ管理のrebind<_Tp>::otherを呼び出しています。
例えば赤黒木のソースコード構造:
typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template
rebind<_Rb_tree_node<_Val> >::other _Node_allocator;
struct _Rb_tree_impl : public _Node_allocator
{
// do somethings
};
次に、上に挙げたメモリ管理クラスの中のソースコード構造を見てみましょう。ここではallocatorを例に取ります:
template<typename _Tp>
class allocator: public __allocator_base<_Tp>
{
template<typename _Tp1>
struct rebind { typedef allocator<_Tp1> other; };
};
見えましたか?rebind<_Tp>::otherを通して、渡されてきたメモリアロケータ、すなわち先ほど触れたこれらを取得しています。
std::allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::__mt_alloc<_Tp>
__gnu_cxx::__pool_alloc<_Tp>
__gnu_cxx::malloc_allocator<_Tp>
これらが理解できたので、テストしてみましょう:
void print() {
cout<<sizeof(std::allocator<int>)<<" "<<sizeof(std::allocator<int>::rebind<int>::other)<<endl;
cout<<sizeof(__gnu_cxx::bitmap_allocator<int>)<<" "<<sizeof(__gnu_cxx::bitmap_allocator<int>::rebind<int>::other)<<endl;
cout<<sizeof(__gnu_cxx::new_allocator<int>)<<" "<<sizeof(__gnu_cxx::new_allocator<int>::rebind<int>::other)<<endl;
cout<<sizeof(__gnu_cxx::__mt_alloc<int>)<<" "<<sizeof(__gnu_cxx::__mt_alloc<int>::rebind<int>::other)<<endl;
cout<<sizeof(__gnu_cxx::__pool_alloc<int>)<<" "<<sizeof(__gnu_cxx::__pool_alloc<int>::rebind<int>::other)<<endl;
cout<<sizeof(__gnu_cxx::malloc_allocator<int>)<<" "<<sizeof(__gnu_cxx::malloc_allocator<int>::rebind<int>::other)<<endl;
}
これらのsizeofが1かどうかテストします。テストした結果、出力は次の通りです:
つまり、メモリ管理の実装は継承の方式を採用し、空基底クラス最適化を使うことで、コンテナが占めるサイズをできるだけ小さくしているのです。
4.EBOを利用して、簡単なメモリの割り当てと解放を手動で実装する¶
まず、sizeof(class)=1のクラスを定義します。STLと同様に、その中でallocateとdeallocateを使ってメモリ管理を行います。
class MyAllocator {
public:
void *allocate(std::size_t size) {
return std::malloc(size);
}
void deallocate(void *ptr) {
std::free(ptr);
}
};
1つ目の方式のメモリ管理:メモリ管理クラスを埋め込む
template<class T, class Allocator>
class MyContainerNotEBO {
T *data_ = nullptr;
std::size_t capacity_;
Allocator allocator_; // 嵌入一个MyAllocator
public:
MyContainerNotEBO(std::size_t capacity)
: capacity_(capacity), allocator_(), data_(nullptr) {
std::cout << "alloc malloc" << std::endl;
data_ = reinterpret_cast<T *>(allocator_.allocate(capacity * sizeof(T))); // 分配内存
}
~MyContainerNotEBO() {
std::cout << "MyContainerNotEBO free malloc" << std::endl;
allocator_.deallocate(data_);
}
};
2つ目の方式:空基底クラス最適化を採用し、継承によってメモリ管理の機能を得る
template<class T, class Allocator>
class MyContainerEBO
: public Allocator { // 继承一个EBO
T *data_ = nullptr;
std::size_t capacity_;
public:
MyContainerEBO(std::size_t capacity)
: capacity_(capacity), data_(nullptr) {
std::cout << "alloc malloc" << std::endl;
data_ = reinterpret_cast<T *>(this->allocate(capacity * sizeof(T)));
}
~MyContainerEBO() {
std::cout << "MyContainerEBO free malloc" << std::endl;
this->deallocate(data_);
}
};
テスト開始:
int main() {
MyContainerNotEBO<int, MyAllocator> notEbo = MyContainerNotEBO<int, MyAllocator>(0);
std::cout << "Using Not EBO Test sizeof is " << sizeof(notEbo) << std::endl;
MyContainerEBO<int, MyAllocator> ebo = MyContainerEBO<int, MyAllocator>(0);
std::cout << "Using EBO Test sizeof is " << sizeof(ebo) << std::endl;
return 0;
}
テスト結果:
alloc malloc
Using Not EBO Test sizeof is 24
alloc malloc
Using EBO Test sizeof is 16
MyContainerEBO free malloc
MyContainerNotEBO free malloc
EBOを使った設計は埋め込み設計よりも確かにかなり優れていることがわかりました。以上で、本節の学習は終わりです。
関連トピック¶
- STL array ソースコード解析 | C++那些事
- STL string 深入解析 | C++那些事
- STL string 面接問題解説 | C++那些事
- STL deque ソースコード解析 | C++那些事
- STL 手書きハッシュテーブル実装 | C++那些事
- STL hashtable ハッシュテーブル | C++那些事
- STL イテレータソースコード解析 | C++那些事
- STL list ソースコード解析 | C++那些事
- STL map と multimap | C++那些事
- STL queue と stack ソースコード解析 | C++那些事