Última atualização:2026-10-12
STL otimização de classe base vazia EBO¶
0. Introdução¶
EBO é a abreviação de Empty Base Optimization.
Nesta seção, começamos pelas classes vazias, passamos pelo interior da STL, pelos testes e, por fim, implementamos nós mesmos um EBO, comparamos o desempenho, testamos de novo e concluímos.
1. Classes vazias¶
Defina uma classe vazia: sem variáveis-membro, sem herança, sem elementos de dados.
Já que ela é vazia, qual é o seu sizeof?
O resultado é 1 — se ela é vazia, por que não é 0?
Porque uma classe vazia ainda pode ser instanciada, e cada instância tem um endereço único na memória. Para atingir esse objetivo, o compilador costuma acrescentar implicitamente um byte à classe vazia, de modo que, após a instanciação, ela obtenha um endereço único na memória. Por isso, o tamanho acima é 1.
Com base na resposta acima, imagino que surja outra pergunta: por que os endereços de dois objetos diferentes devem ser diferentes?
Veja este exemplo:
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;
}
Vamos testar:
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;
}
O teste acima mostra que dois objetos diferentes têm endereços diferentes; considere o cenário a seguir:
Neste caso, percebe-se que, se o endereço de a fosse igual ao de b, ter dois objetos no mesmo endereço significaria não conseguir distinguir os dois objetos ao referenciá-los por ponteiro. Portanto, objetos diferentes têm endereços diferentes.
2. Otimização de classe base vazia¶
Agora compare os dois usos a seguir. No primeiro, uma classe contém outra classe como membro e, por meio dela, obtém as funcionalidades da classe contida.
No outro, adota-se diretamente a herança para obter as funções-membro da classe base e outras funcionalidades.
Em seguida, faremos um teste:
Saída:
No primeiro caso, por causa do alinhamento de bytes, o membro que ocupava apenas 1 byte é expandido para um múltiplo de 4, resultando em 8 bytes.
Comparando os dois, descobre-se que o segundo, que obtém as funcionalidades da classe base por herança, não gera tamanho adicional — essa otimização é chamada de EBO (Empty Base Optimization, otimização de classe base vazia).
Agora, voltemos ao código-fonte da STL para ver o seu uso!
3. O mundo do EBO na STL¶
Seja em deque, rb_tree, list ou outros contêineres, todos dependem de gerenciamento de memória; cada um deles contém o gerenciamento de memória correspondente e, por meio de _xx_impl, herda as classes a seguir:
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>
Mas o que isso tem a ver com o nosso EBO?
Na verdade, as classes base listadas acima como herdadas são todas gerenciadores de memória do tipo EBO (classes base vazias).
Em cada contêiner, o uso se dá chamando o rebind<_Tp>::other de cada gerenciador de memória.
Por exemplo, a estrutura do código-fonte da red-black tree:
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
};
A seguir, vejamos a estrutura do código-fonte das classes de gerenciamento de memória listadas acima, tomando o allocator como exemplo:
template<typename _Tp>
class allocator: public __allocator_base<_Tp>
{
template<typename _Tp1>
struct rebind { typedef allocator<_Tp1> other; };
};
Viu? Por meio de rebind<_Tp>::other obtém-se o alocador de memória passado — exatamente os citados antes.
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>
Entendido isso, vamos fazer uma rodada de testes:
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;
}
Vamos testar se esses sizeof são 1; após o teste, a saída foi a seguinte:
Isso mostra que a implementação do gerenciamento de memória adota a herança, usando a otimização de classe base vazia, para reduzir o máximo possível o tamanho ocupado pelo contêiner.
4. Usando EBO, implementar manualmente uma alocação e liberação de memória simples¶
Primeiro, defina uma classe com sizeof(class)=1 que, como na STL, use allocate e deallocate para o gerenciamento de memória.
class MyAllocator {
public:
void *allocate(std::size_t size) {
return std::malloc(size);
}
void deallocate(void *ptr) {
std::free(ptr);
}
};
Primeira forma de gerenciamento de memória: embutir uma classe de gerenciamento de memória
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_);
}
};
Segunda forma: adotar a otimização de classe base vazia, obtendo a funcionalidade de gerenciamento de memória por herança
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_);
}
};
Hora de testar:
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;
}
Resultado do teste:
alloc malloc
Using Not EBO Test sizeof is 24
alloc malloc
Using EBO Test sizeof is 16
MyContainerEBO free malloc
MyContainerNotEBO free malloc
Descobrimos que o desenho com EBO é realmente muito melhor do que o desenho com embutimento. Com isso, conclui-se esta seção.
Tópicos relacionados¶
- Análise do código-fonte do STL array | C++那些事
- Análise aprofundada do STL string | C++那些事
- Questões de entrevista sobre STL string explicadas | C++那些事
- Análise do código-fonte do STL deque | C++那些事
- STL implementação manual de tabela hash | C++那些事
- STL hashtable tabela hash | C++那些事
- Análise do código-fonte dos iteradores da STL | C++那些事
- Análise do código-fonte do STL list | C++那些事
- STL map e multimap | C++那些事
- Análise do código-fonte do STL queue e stack | C++那些事