Saltar a contenido

Última actualización:2026-10-12

STL optimización de clase base vacía EBO

0. Introducción

EBO es el acrónimo de Empty Base Optimization.

Esta sección empieza con las clases vacías, pasa al interior de STL, continúa con pruebas, luego implementamos nosotros mismos un EBO, comparamos el rendimiento y, por último, volvemos a probar y sacamos conclusiones.

1. Clase vacía

Definimos una clase vacía: una clase sin variables miembro, sin herencia y sin elementos de datos.

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

Como está vacía, ¿cuánto vale su sizeof?

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

El resultado es 1. Si está vacía, ¿por qué no es 0?

Porque una clase vacía también puede instanciarse, y cada instancia debe tener una dirección única en memoria. Para lograrlo, el compilador suele añadir implícitamente un byte a la clase vacía, de modo que, una vez instanciada, obtiene una dirección única en memoria. Por eso el tamaño anterior es 1.

A raíz de la respuesta anterior, seguramente surge otra pregunta: ¿por qué dos objetos distintos deben tener direcciones diferentes?

Tomemos el siguiente ejemplo:

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 a probarlo:

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;
}

Arriba hemos comprobado que dos objetos distintos tienen direcciones distintas; consideremos el siguiente escenario:

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

En este momento se observa que, si la dirección de a fuese igual a la dirección de b, tener dos objetos en la misma dirección significaría que sería imposible distinguirlos al referirse a ellos mediante punteros. Por eso dos objetos distintos tienen direcciones diferentes.

2. Optimización de clase base vacía

Comparemos ahora los dos usos siguientes. En el primero, una clase contiene a otra como miembro y obtiene así la funcionalidad de la clase contenida.

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

En el otro, se adopta directamente la herencia para obtener las funciones miembro de la base y demás funcionalidades.

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

A continuación, hagamos una prueba:

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

Salida:

8 
4

En el primer caso, debido a la alineación de bytes, el miembro que originalmente solo ocupaba 1 byte se rellena hasta un múltiplo de 4, con lo que el total son 8 bytes.

Comparando ambos casos se descubre que el segundo, que obtiene la funcionalidad de la clase base mediante herencia, no genera un tamaño adicional: esta optimización se denomina EBO (Empty Base Optimization, optimización de clase base vacía).

A continuación, volvamos al código fuente de STL para ver cómo se usa.

3. El mundo del EBO en STL

Tanto deque, rb_tree, list como otros contenedores no pueden prescindir de la gestión de memoria; todos ellos incorporan su correspondiente gestión de memoria y, mediante _xx_impl, heredan de las clases siguientes:

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>

¿Y qué relación tiene esto con nuestro EBO?

En realidad, las clases base de las que se hereda en la lista anterior son todas EBO (clases base vacías) de gestión de memoria.

En cada contenedor, el uso pasa por invocar el rebind<_Tp>::other de cada gestor de memoria.

Por ejemplo, la estructura del código fuente del árbol rojo-negro:

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
};

Veamos ahora la estructura del código fuente de las clases de gestión de memoria listadas arriba, tomando allocator como ejemplo:

template<typename _Tp>
class allocator: public __allocator_base<_Tp>
{
     template<typename _Tp1>
     struct rebind { typedef allocator<_Tp1> other; };
};

¿Lo ves? Mediante rebind<_Tp>::other se obtiene el asignador de memoria pasado, es decir, los mencionados 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 esto, hagamos una tanda de pruebas:

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;
}

Comprobemos si estos sizeof valen 1; tras la prueba, la salida es la siguiente:

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

Esto muestra que la implementación de la gestión de memoria consiste precisamente en adoptar la herencia y usar la optimización de clase base vacía para reducir todo lo posible el tamaño ocupado por el contenedor.

4. Usar EBO para implementar a mano una asignación y liberación de memoria sencillas

Primero definimos una clase con sizeof(class)=1 que, igual que en STL, use allocate y deallocate para gestionar la memoria.

class MyAllocator {
public:
    void *allocate(std::size_t size) {
        return std::malloc(size);
    }

    void deallocate(void *ptr) {
        std::free(ptr);
    }
};

Primera forma de gestionar la memoria: incrustar una clase de gestión de memoria

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: adoptar la optimización de clase base vacía y obtener la funcionalidad de gestión de memoria mediante herencia

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_);
    }
};

Empiezan las pruebas:

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 de la prueba:

alloc malloc
Using Not EBO Test sizeof is 24
alloc malloc
Using EBO Test sizeof is 16
MyContainerEBO free malloc
MyContainerNotEBO free malloc

Comprobamos que el diseño con EBO es, en efecto, mucho mejor que el diseño con incrustación. Con esto termina el estudio de esta sección.

Temas relacionados

Comentarios