Muchas veces necesitamos reservar memoria dinámica durante la ejecución de un programa. Sin embargo, esto es un proceso relativamente lento, porque el sistema operativo debe buscar un hueco suficientemente grande en el heap, donde se almacenan los objetos dinámicos. Esto se nota, por ejemplo, cuando uno quiere reservar muchos elementos de una lista. En este artículo quiero presentar algunas propuestas, como lenguajes compilados ofrecen alternativas más veloces. Los ejemplos de código serán en C++.

Contenedores pueden instanciar varios elementos de golpe

El tamaño no importa (mucho) a la hora de reservar memoria. Es el número de veces que afecta más. Una primera posibilidad que se ofrece, por lo tanto, es reservar memoria para varios elementos de golpe. Por supuesto, esto es menos optimal respecto al uso de la memoria, pero una técnica ampliamente usado por los contenedores de la biblioteca estándar de C++ (STL).

Un contenedor puede usar el operador new[] estándar para reservar un array de, digamos, 10 elementos y luego gestionar, cuales elementos ya están en uso y cuales no. Esto puede ser relativamente complicado. Sin embargo, se puede hacer a medida para una clase contenedor y también cuando le haga falta. Por lo tanto, es una buena opción.

Sobrecargar el operador new.

Otra opción para una clase, que está ideada para instanciarse a menudo de forma dinámica, es sobrescribir el operador new (y delete) de esta clase. La implementación de este operador sobrecargado suele tener dos elementos. Primero usa el operador new global para pedir memoria al sistema operativo. Segundo, las instancias reservadas se gestionarán por unas variables estáticas de la clase. De esta forma, el depósito de elementos reservados no depende de una instancia en concreto – que es justamente lo que queremos.

El operador new global debe ser precidido por el operador :: para distinguirle del operador sobrecargado. Es decir new MiClase() llamaría al operador sobrecargado, mientras ::new MiClase() al sistema operativo. Si el operador no está sobrecargado, ambas notaciones tienen el mismo efecto. Lo mismo vale para el operador delete.

Separar reserva de memoria y construcción de instancia

La desventaja en pedir muchos elementos de golpe es que se llama el constructor por defecto para todos estos elementos aunque no se usan a priori. Pero a esto hay remedio, porque en C++ se puede separar la reserva de la memoria dinámica de la construcción de una instancia. Si, en lugar del tipo de la clase, reservo un array de char, entonces la reserva no llama a ningún constructor. Cuando quiero instanciar un nuevo elemento llamo al constructor mediante

void* operator new (std::size_t size, void* ptr) throw();

Esta versión del operador new no reserva memoria. Simplemente devuelve el puntero ptr. La manera de construir una instancia de la clase TElement en la memoria ya reservada apuntada por memory_ptr sería

*memory_ptr = * new ((TElement*) memory_ptr) TElement(*source_ptr);

Source_ptr podría ser otra instancia de la misma clase de donde copiar – es decir, la construcción arriba llamaría al constructor de copia. Los parámetros de new son tan variables como los parámetros de los constructores de la clase.

Para destruir un elemento sin liberar memoria llamo explícitamente a su destructor.


(MiClase* ptr)->~MiClase();

Reservar memoria en el stack

La forma más rápida es reservar memoria en el stack en lugar del heap. Esta reserva sucede cada vez que declaro una variable dentro de una función. Si sé que mi cadena de texto no tendrá más que mil bytes, entonces


char a[1001];

consiste en incrementar el puntero del stack, mientras


char* b = new char[1001];

en la búsqueda de un hueco lo suficientemente grande en el heap. La primera manera es muchísimo más rápida que la segunda. Teniendo en cuenta que la reserva dinámica puede, además, introducir errores de programación por olvidarse de liberar la memoria, es casi siempre preferible a ser generoso con la memoria RAM. Los ordenadores de hoy en día tienen de sobra.

Esta reserva en el stack se puede combinar con una gestión individualizada de la memoria. Dentro de mi clase puedo tener un miembro (posiblemente estático)


private: char _memory[1000];

Puedo convertirlo a un objeto de tipo TElement en la forma

TElement* element_ptr(void)
{
    return reinterpret_cast<TElement>(static_cast<char*>(_memory));
}

El static_cast no es obligatorio. Por la complejidad de la construcción conviene encapsularla en un método privado que convierte el almacén de memoria al tipo de clase requerido.

Hay implementaciones de la STL que usan esta técnica. Por ejemplo, la clase std::string contiene un búfer pequeño como miembro de la clase. Esto hace el almacenamiento de cadenas de textos cortos rápido. Si la longitud del string excede el tamaño del búfer, entonces se pide memoria dinámica.

Allocators

Los contenedores de la STL suministran otro mecanismo de cambiar la gestión de memoria mediante una clase allocator. Este allocator aparece como un parámetro en las plantillas de los contenedores. Aunque es una forma sofisticada de separar la gestión de memoria del funcionamiento del contenedor, aporta un problema nuevo: un contenedor con un allocator diferente no tiene el mismo tipo que un contendor con el allocator por defecto. Por lo tanto, dos instancias del mismo contenedor se comportan como dos contenedores distintos.

std::list<int, MiAllocator> miLista;
std::list<int> listaNormal;
listaNormal = miLista // Error: listaNormal y miLista no son de la misma clase

Sin embargo, los contenedores pueden interactuar al nivel de iteradores.

// Esto está permitido.
listaNormal.insert(listaNormal.begin(), miLista.begin(), miLista.end());

Si estás pensando en utilizar allocators personalizados, entonces considerar obligar el uso del contenedor modificado por toda la aplicación, por ejemplo utilizando un typedef que define un tipo de lista propia derivada de la STL.

Conclusión

Hemos visto varias formas de como hacer más rápida la creación de memoria dinámica. El incremento en velocidad se suele pagar por un uso menos óptimo de la memoria. Sin embargo, en la mayoría de los casos, no falta memoria. La gestión individual de la memoria es una tarea compleja y requiere cuidado. Un mal algoritmo para guardar, qué elementos reservados están en uso y cuales no, puede fácilmente acabar con la ventaja, que la administración propia de la memoria podría traer.

En C++ se puede separar la reserva de memoria de la construcción de instancias. La biblioteca STL permite utilizar gestores de memorias propias. Es una herramienta potente pero que puede traer complicaciones a la hora de mezclarla con contenedores con el gestor estándar.

Referencias

Anuncios