Ú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.
Como está vacía, ¿cuánto vale su sizeof?
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:
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.
En el otro, se adopta directamente la herencia para obtener las funciones miembro de la base y demás funcionalidades.
A continuación, hagamos una prueba:
Salida:
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:
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¶
- Análisis del código fuente de STL array | C++那些事
- Análisis profundo de STL string | C++那些事
- STL string preguntas de entrevista explicadas | C++那些事
- Análisis del código fuente de STL deque | C++那些事
- STL implementación de una tabla hash escrita a mano | C++那些事
- STL hashtable tabla hash | C++那些事
- Análisis del código fuente de los iteradores STL | C++那些事
- Análisis del código fuente de STL list | C++那些事
- STL map y multimap | C++那些事
- Análisis del código fuente de STL queue y stack | C++那些事