Ir para o conteúdo

Ú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.

class Empty{
public:
    void print() {
        std::cout<<"I am Empty class"<<std::endl;
    }
};

Já que ela é vazia, qual é o seu sizeof?

std::cout<<sizeof(Empty)<<std::endl; //1

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:

Empty a,b;
// 在同一地址具有两个对象将意味着在使用指针引用它们时将无法区分这两个对象。
Empty *p1=&a;
p1->print();

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.

class notEbo  {
    int i;
    Empty e;
    // do other things
};

No outro, adota-se diretamente a herança para obter as funções-membro da classe base e outras funcionalidades.

class ebo:public Empty {
    int i;
    // do other things
};

Em seguida, faremos um teste:

std::cout<<sizeof(notEbo)<<std::endl;
std::cout<<sizeof(ebo)<<std::endl;

Saída:

8 
4

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:

1 1
1 1
1 1
1 1
1 1
1 1

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

Comentários