c++ memory-management memory-pool

c++ - ¿Cuáles son los detalles de implementación habituales detrás de las agrupaciones de memoria?



memory-management memory-pool (3)

Estoy tratando de entender el uso de grupos de memoria para la administración de memoria, pero no puedo encontrar mucho al respecto, aunque parece ser un mecanismo muy común.

Todo lo que sé de esto es que los "grupos de memoria, también llamados asignación de bloques de tamaño fijo" por Wikipedia, y puedo usar esos fragmentos para asignar memoria a mis objetos.

¿Hay alguna especificación estándar sobre los grupos de memoria?

Me gustaría saber cómo funciona esto en el montón, cómo se puede implementar y cómo se debe usar.

De esta pregunta sobre los patrones de diseño de la agrupación de memoria C ++ 11 , he leído:

Si aún no lo has hecho, familiarízate con Boost.Pool. De la documentación de Boost:

¿Qué es la piscina?

La asignación de grupo es un esquema de asignación de memoria que es muy rápido, pero limitado en su uso. Para obtener más información sobre la asignación de grupos (también llamado almacenamiento simple segregado, vea los con­cepts conceptos y Almacenamiento simple segregado).

Puedo entender lo que quiso decir, pero eso no me ayuda a entender cómo usarlos y cómo los pools de memoria pueden ayudar a mi aplicación, cómo usarlos realmente.

Se agradecería un ejemplo simple que muestre cómo usar los grupos de memoria.


Básicamente, las agrupaciones de memoria le permiten evitar parte del gasto de asignar memoria en un programa que asigna y libera la memoria con frecuencia. Lo que hace es asignar una gran parte de la memoria al comienzo de la ejecución y reutilizar la misma memoria para diferentes asignaciones que no se superpongan temporalmente. Debe tener algún mecanismo para realizar un seguimiento de la memoria disponible y utilizar esa memoria para las asignaciones. Cuando haya terminado con la memoria, en lugar de liberarla, márquela como disponible nuevamente.

En otras palabras, en lugar de las llamadas a new / malloc y delete / free , realice una llamada a las funciones de asignador / desasignador definidas por el usuario.

Hacer esto le permite hacer solo una asignación (suponiendo que sepa aproximadamente la cantidad de memoria que necesitará en total) en el curso de la ejecución. Si su programa está vinculado a la latencia, en lugar de a la memoria, puede escribir una función de asignación que funcione más rápido que malloc a expensas de un cierto uso de memoria.


Cualquier tipo de "grupo" es realmente solo recursos que ha adquirido / inicializado por adelantado para que estén listos para usar, no asignados al vuelo con cada solicitud de cliente. Cuando los clientes terminan de usarlos, el recurso vuelve al grupo en lugar de ser destruido.

Los grupos de memoria son básicamente solo memoria que has asignado de antemano (y normalmente en grandes bloques). Por ejemplo, puede asignar 4 kilobytes de memoria por adelantado. Cuando un cliente solicita 64 bytes de memoria, simplemente les entrega un puntero a un espacio no utilizado en ese grupo de memoria para que lean y escriban lo que quieran. Cuando el cliente haya terminado, puede marcar esa sección de la memoria como no utilizada nuevamente.

Como un ejemplo básico que no se preocupa por la alineación, la seguridad o la devolución de la memoria no utilizada (liberada) al grupo:

class MemoryPool { public: MemoryPool(): ptr(mem) { } void* allocate(int mem_size) { assert((ptr + mem_size) <= (mem + sizeof mem) && "Pool exhausted!"); void* mem = ptr; ptr += mem_size; return mem; } private: MemoryPool(const MemoryPool&); MemoryPool& operator=(const MemoryPool&); char mem[4096]; char* ptr; }; ... { MemoryPool pool; // Allocate an instance of `Foo` into a chunk returned by the memory pool. Foo* foo = new(pool.allocate(sizeof(Foo))) Foo; ... // Invoke the dtor manually since we used placement new. foo->~Foo(); }

Esto es efectivamente simplemente agrupando la memoria de la pila. Una implementación más avanzada podría encadenar bloques y hacer algunas derivaciones para ver si un bloque está lleno para evitar quedarse sin memoria, lidiar con trozos de tamaño fijo que son uniones (enumerar nodos cuando están libres, memoria para el cliente cuando se usa), y definitivamente necesita lidiar con la alineación (la forma más sencilla es alinear al máximo los bloques de memoria y agregar relleno a cada fragmento para alinear el siguiente).

Más sofisticados serían los asignadores de amigos, losas, los que aplican algoritmos de ajuste, etc. La implementación de un asignador no es tan diferente de una estructura de datos, pero se obtiene hasta las rodillas en bits y bytes en bruto, hay que pensar en cosas como la alineación y se puede " t barajar el contenido (no se pueden invalidar los punteros existentes de la memoria que se está utilizando). Al igual que las estructuras de datos, en realidad no hay un estándar de oro que diga "debes hacer esto". Hay una gran variedad de ellos, cada uno con sus propias fortalezas y debilidades, pero hay algunos algoritmos especialmente populares para la asignación de memoria.

La implementación de asignadores es algo que realmente recomendaría a muchos desarrolladores de C y C ++ solo para estar al tanto de la forma en que la administración de memoria funciona un poco mejor. Puede hacerlo un poco más consciente de cómo la memoria solicitada se conecta a las estructuras de datos que los utilizan, y también abre una nueva puerta de oportunidades de optimización sin utilizar ninguna estructura de datos nueva. También puede hacer que las estructuras de datos, como las listas vinculadas, que normalmente no son muy eficientes, sean mucho más útiles y reducir las tentaciones de hacer que los tipos opacos / abstractos sean menos opacos para evitar la sobrecarga del montón. Sin embargo, puede haber una emoción inicial que podría querer que se conviertan en calzadores personalizados para todo, solo para luego lamentar la carga adicional (especialmente si, en su emoción, se olvida de problemas como la alineación y la seguridad del hilo). Vale la pena tomarse las cosas con calma allí. Al igual que con cualquier microoptimización, generalmente se aplica mejor de forma discreta, en retrospectiva, y con un perfilador en la mano.


El concepto básico de una agrupación de memoria es asignar una gran parte de la memoria a su aplicación y, más adelante, en lugar de usar una new de solicitar la memoria de la O / S, en su lugar, devuelve una parte de la memoria asignada anteriormente.

Para hacer que esto funcione, usted necesita administrar el uso de la memoria y no puede confiar en el O / S; es decir, deberá implementar sus propias versiones de new y delete , y usar las versiones originales solo cuando asigne, libere o posiblemente cambie su tamaño al conjunto de memoria.

El primer enfoque sería definir la propia Clase que encapsule un grupo de memoria y proporcione métodos personalizados que implementen la semántica de new y delete , pero que tomen la memoria del grupo asignado previamente. Recuerde, esta agrupación no es más que un área de memoria que se ha asignado utilizando new y tiene un tamaño arbitrario. La versión del grupo de new / delete return resp. tomar punteros. La versión más simple probablemente se vería como el código C:

void *MyPool::malloc(const size_t &size) void MyPool::free(void *ptr)

Puede salpicar esto con plantillas para agregar automáticamente la conversión, por ejemplo,

template <typename T> T *MyClass::malloc(); template <typename T> void MyClass::free(T *ptr);

Tenga en cuenta que, gracias a los argumentos de la plantilla, el argumento size_t size se puede omitir ya que el compilador le permite llamar a sizeof(T) en malloc() .

Devolver un puntero simple significa que su grupo solo puede crecer cuando hay memoria adyacente disponible, y solo se reduce si no se toma la memoria del grupo en sus "bordes". Más específicamente, no puede reubicar el grupo porque eso invalidaría todos los punteros que devolvió su función malloc.

Una forma de solucionar esta limitación es devolver los punteros a los punteros, es decir, devolver T** lugar de simplemente T* . Eso le permite cambiar el puntero subyacente, mientras que la parte que se enfrenta al usuario sigue siendo la misma. Incidencialmente, eso se ha hecho para el NeXT O / S, donde se llamó "manejador". Para acceder al contenido del manejador, uno tuvo que llamar (*handle)->method() , o (**handle).method() . Finalmente, Maf Vosburg inventó un pseudo-operador que explotaba la precedencia del operador para deshacerse de la sintaxis (*handle)->method() : handle[0]->method(); Se llamaba el operador de sprong .

Los beneficios de esta operación son: Primero, evita la sobrecarga de una llamada típica a new y delete , y segundo, su grupo de memoria asegura que su aplicación utilice un segmento de memoria contiguo, es decir, evita la fragmentación de la memoria y, por lo tanto, aumenta Aciertos de caché de CPU.

Así que, básicamente, un grupo de memoria le proporciona una aceleración que obtiene con el inconveniente de un código de aplicación potencialmente más complejo. Pero, de nuevo, hay algunas implementaciones de grupos de memoria que están probadas y que pueden ser utilizadas simplemente, como boost::pool .