2009-05-05 16 views
146

¿Cuáles son algunas buenas razones para abandonar std::allocator a favor de una solución personalizada? ¿Te has encontrado con situaciones en las que era absolutamente necesario para la corrección, el rendimiento, la escalabilidad, etc.? ¿Algún ejemplo realmente inteligente?¿Ejemplos convincentes de asignadores de C++ personalizados?

Los asignadores personalizados siempre han sido una característica de la Biblioteca estándar que no he tenido mucha necesidad. Me preguntaba si alguien aquí en SO podría proporcionar algunos ejemplos convincentes para justificar su existencia.

Respuesta

91

Como menciono here, he visto STL asignador de encargo de Intel TBB mejorar significativamente el rendimiento de una aplicación de multiproceso simplemente cambiando una sola

std::vector<T> 

a

std::vector<T,tbb::scalable_allocator<T> > 

(esta es una rápida y una forma conveniente de cambiar el asignador para utilizar ingeniosos montones de hilos privados de TBB; consulte page 7 in this document)

+1

Gracias por ese segundo enlace. El uso de asignadores para implementar montones de hilos privados es inteligente. Me gusta que este sea un buen ejemplo de dónde los asignadores personalizados tienen una clara ventaja en un escenario que no tiene recursos limitados (embebido o consola). – Naaff

+4

El enlace original ya no está, pero CiteSeer tiene el PDF: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.8289 –

+1

Tengo que preguntar: ¿Puedes mover ese vector de manera confiable? en otro hilo? (Supongo que no) – sellibitze

15

Puede ser útil usar asignadores personalizados para usar un grupo de memoria en lugar del montón. Ese es un ejemplo entre muchos otros.

En la mayoría de los casos, esta es sin duda una optimización prematura. Pero puede ser muy útil en ciertos contextos (dispositivos integrados, juegos, etc.).

+2

O, cuando se comparte ese grupo de memoria. – Anthony

7

No he escrito el código C++ con un asignador STL personalizado, pero puedo imaginar un servidor web escrito en C++, que utiliza un asignador personalizado para la eliminación automática de datos temporales necesarios para responder a una solicitud HTTP. El asignador personalizado puede liberar todos los datos temporales a la vez una vez que se ha generado la respuesta.

Otro posible caso de uso para un asignador personalizado (que he usado) es escribir una prueba de unidad para demostrar que el comportamiento de una función no depende de alguna parte de su entrada. El asignador personalizado puede llenar la región de memoria con cualquier patrón.

+3

Parece que el primer ejemplo es el trabajo del destructor, no el asignador. – anthropomorphic

+1

Si le preocupa su programa dependiendo del contenido inicial de la memoria del montón, una ejecución rápida (es decir, ¡durante la noche!) En valgrind le permitirá saber de una forma u otra. – cdyson37

+1

@anthropomorphic: el destructor y el asignador personalizado funcionarían juntos, el destructor se ejecutaría primero, luego la eliminación del asignador personalizado, que no se llamará gratis (...) todavía, pero libre (...) sería llamado más tarde, cuando se ha completado la entrega de la solicitud.Esto puede ser más rápido que el asignador predeterminado y reducir la fragmentación del espacio de direcciones. – pts

22

Estoy trabajando con MySQL s motor de carga que usa C++ para su código. Estamos utilizando un asignador personalizado para usar el sistema de memoria MySQL en lugar de competir con MySQL por la memoria. Nos permite asegurarnos de que estamos usando la memoria como el usuario configurado para usar MySQL, y no "extra".

69

Un área donde los asignadores personalizados pueden ser útiles es el desarrollo de juegos, especialmente en las consolas de juegos, ya que solo tienen una pequeña cantidad de memoria y ningún intercambio. En dichos sistemas, debe asegurarse de tener un control estricto sobre cada subsistema, de modo que un sistema no crítico no pueda robar la memoria de uno de los críticos. Otras cosas como los asignificadores de grupo pueden ayudar a reducir la fragmentación de la memoria. Puede encontrar un documento largo, detallado sobre el tema en:

EASTL -- Electronic Arts Standard Template Library

+10

+1 para el enlace EASTL: "Entre los desarrolladores de juegos, la debilidad más fundamental [del STL] es el diseño del asignador estándar, y es esta debilidad el factor que más contribuyó a la creación de EASTL". – Naaff

5

estoy usando asignadores personalizados aquí; incluso podría decir que fue para funcionar alrededor de otra gestión de memoria dinámica personalizada.

Antecedentes: tenemos sobrecargas para malloc, calloc, free, y las diversas variantes de operador new y delete, y el enlazador hace felizmente que STL las use para nosotros. Esto nos permite hacer cosas como la agrupación automática de objetos pequeños, la detección de fugas, el relleno de alloc, el relleno libre, la asignación de relleno con centinelas, la alineación de la línea de caché para ciertos allocs y el retraso de la liberación.

El problema es que nos estamos ejecutando en un entorno incrustado: no hay memoria suficiente para realizar la detección de fugas correctamente durante un período prolongado. Al menos, no en la RAM estándar: hay otro montón de RAM disponible en otra parte, a través de funciones de asignación personalizadas.

Solución: escribir un asignador personalizado que utiliza el montón extendida, y utilizarlo solamente en la parte interna de la arquitectura de seguimiento pérdida de memoria ... Todo lo demás valores por defecto a la nueva normalidad/eliminar las sobrecargas que se escapan de seguimiento. Esto evita que el rastreador se rastree a sí mismo (y también proporciona un poco de funcionalidad adicional de embalaje, conocemos el tamaño de los nodos del rastreador).

También usamos esto para mantener los datos de perfil de costo de función, por el mismo motivo; escribir una entrada para cada función de llamada y devolución, así como los conmutadores de subprocesos, puede ser costoso rápidamente. El asignador personalizado nuevamente nos da allocs más pequeños en un área de memoria de depuración más grande.

4

Estoy usando un asignador personalizado para contar el número de asignaciones/desasignaciones en una parte de mi programa y medir cuánto tiempo lleva. Hay otras maneras en que esto se puede lograr, pero este método es muy conveniente para mí. Es especialmente útil que pueda usar el asignador personalizado solo para un subconjunto de mis contenedores.

3

Una situación esencial: al escribir código que debe funcionar entre los límites del módulo (EXE/DLL), es esencial mantener sus asignaciones y eliminaciones en un solo módulo.

Donde me topé con esto era una arquitectura de complemento en Windows. Es esencial que, por ejemplo, si pasa una cadena std :: a través del límite de la DLL, que cualquier reasignación de la cadena ocurra desde el montón de donde se originó, NO el montón en la DLL que puede ser diferente *.

* Es más complicado que esto en realidad, como si estuvieras enlazando dinámicamente con el CRT esto podría funcionar de todos modos. Pero si cada DLL tiene un enlace estático al CRT, se dirige a un mundo de dolor, donde continuamente ocurren errores de asignación fantasma.

+0

Si pasa objetos a través de los límites de la DLL, debe utilizar la configuración de subprocesos múltiples (depuración) de las DLL (/ MD (d)) para ambas partes. C++ no se diseñó teniendo en cuenta el soporte de módulos. Alternativamente, podría proteger todo lo que está detrás de las interfaces COM y usar CoTaskMemAlloc. Esta es la mejor manera de utilizar interfaces de complementos que no están vinculadas a un compilador, STL o proveedor específico. – gast128

53

Estoy trabajando en un mmap-allocator que permite a los vectores utilizar la memoria de un archivo mapeado en memoria. El objetivo es tener vectores que usen almacenamiento que estén directamente en la memoria virtual mapeada por mmap. Nuestro problema es mejorar la lectura de archivos realmente grandes (> 10GB) en la memoria sin copia sobrecarga, por lo tanto, necesito este asignador personalizado.

Hasta ahora tengo el esqueleto de un asignador personalizado (que deriva de std :: allocator), creo que es un buen punto de partida para escribir asignadores propios. Siéntase libre de utilizar este pedazo de código en la forma que desee:

#include <memory> 
#include <stdio.h> 

namespace mmap_allocator_namespace 
{ 
     template <typename T> 
     class mmap_allocator: public std::allocator<T> 
     { 
public: 
       typedef size_t size_type; 
       typedef T* pointer; 
       typedef const T* const_pointer; 

       template<typename _Tp1> 
       struct rebind 
       { 
         typedef mmap_allocator<_Tp1> other; 
       }; 

       pointer allocate(size_type n, const void *hint=0) 
       { 
         fprintf(stderr, "Alloc %d bytes.\n", n*sizeof(T)); 
         return std::allocator<T>::allocate(n, hint); 
       } 

       void deallocate(pointer p, size_type n) 
       { 
         fprintf(stderr, "Dealloc %d bytes (%p).\n", n*sizeof(T), p); 
         return std::allocator<T>::deallocate(p, n); 
       } 

       mmap_allocator() throw(): std::allocator<T>() { fprintf(stderr, "Hello allocator!\n"); } 
       mmap_allocator(const mmap_allocator &a) throw(): std::allocator<T>(a) { } 
       template <class U>      
       mmap_allocator(const mmap_allocator<U> &a) throw(): std::allocator<T>(a) { } 
       ~mmap_allocator() throw() { } 
     }; 
} 

Para usar esto, declarar un contenedor STL de la siguiente manera:

using namespace std; 
using namespace mmap_allocator_namespace; 

vector<int, mmap_allocator<int> > int_vec(1024, 0, mmap_allocator<int>()); 

Se puede utilizar por ejemplo para iniciar la sesión cada vez que la memoria está asignado. Lo que es necesario es la estructura de reenlace, de lo contrario, el contenedor de vectores utiliza las superclases asignar/desasignar métodos.

Actualización: El asignador de asignación de memoria ahora está disponible en https://github.com/johannesthoma/mmap_allocator y es LGPL. Siéntase libre de usarlo para sus proyectos.

+2

gracias esto fue muy útil – stack356

+7

Solo un encabezado, derivado de std :: allocator no es realmente la forma idiomática de escribir asignadores. En su lugar, debe buscar allocator_traits, que le permite proporcionar el mínimo de funcionalidad, y la clase de rasgos proporcionará el resto. Tenga en cuenta que el STL siempre usa su asignador a través de allocator_traits, no directamente, por lo que no necesita referirse a allocator_traits usted mismo. No hay mucho incentivo para derivar de std :: allocator (aunque este código puede ser un punto de partida útil independientemente). –

4

Al trabajar con GPU u otros coprocesadores, a veces es beneficioso asignar estructuras de datos en la memoria principal de una manera especial . Esta manera especial de asignar memoria se puede implementar en un asignador personalizado de una manera conveniente.

La razón por la asignación personalizada a través del tiempo de ejecución del acelerador puede ser beneficioso cuando se utiliza aceleradores es la siguiente:

  1. través de la asignación de encargo el tiempo de ejecución del acelerador o del conductor es notificado del bloque de memoria
  2. , además, la operación El sistema puede asegurarse de que el bloque asignado de memoria esté bloqueado (algunos llaman a esto memoria fija), es decir, el subsistema de memoria virtual del sistema operativo no puede mover o eliminar la página dentro o desde la memoria
  3. if 1. y 2. espera y se solicita una transferencia de datos entre un bloque de memoria de bloqueo de página y un acelerador, el tiempo de ejecución puede acceder directamente a los datos en la memoria principal ya que sabe dónde está y puede estar seguro de que el sistema operativo no se movió/eliminarlo
  4. esto guarda una copia de memoria que ocurriría con la memoria asignada sin bloqueo de página: los datos deben copiarse en la memoria principal en un área de ensayo de página bloqueada con el acelerador puede inicializarse la transferencia de datos (a través de DMA)
+1

... no olvidar los bloques de memoria alineados con la página. Esto es especialmente útil si está hablando con un controlador (es decir, con FPGA a través de DMA) y no quiere la molestia ni la sobrecarga de calcular los desplazamientos dentro de la página para sus listas de dispersión DMA. – Jan

1

yo personalmente uso Loki :: Allocator/SmallObject para optimizar el uso de memoria para objetos pequeños - que muestran una buena eficiencia y el rendimiento satisfactorio si se tiene que trabajar con cantidades moderadas de objetos muy pequeños (1 a 256 bytes). Puede ser hasta ~ 30 veces más eficiente que la asignación nueva/eliminar C++ estándar si hablamos de asignar cantidades moderadas de objetos pequeños de diferentes tamaños. Además, hay una solución específica de VC llamada "QuickHeap", que ofrece el mejor rendimiento posible (asignar y desasignar operaciones simplemente leer y escribir la dirección del bloque que se asigna/devuelve al montón, respectivamente en hasta 99. (9)% de casos - depende de la configuración y la inicialización), pero a costa de una sobrecarga notable: necesita dos punteros por extensión y un extra por cada bloque de memoria nuevo. Es la solución más rápida posible para trabajar con enormes cantidades (10 000 ++) de objetos creados y eliminados si no se necesita una gran variedad de tamaños de objetos (se crea un grupo individual para cada tamaño de objeto, de 1 a 1023 bytes) en la implementación actual, por lo que los costos de inicialización pueden reducir el aumento del rendimiento general, pero se puede seguir y asignar/desasignar algunos objetos ficticios antes de que la aplicación entre en su (s) fase (s) crítica (s) de rendimiento.

El problema con la implementación estándar nueva/eliminar de C++ es que normalmente es solo un contenedor para la asignación de C malloc/free, y funciona bien para bloques de memoria más grandes, como 1024+ bytes. Tiene una sobrecarga notable en términos de rendimiento y, a veces, memoria extra utilizada también para el mapeo. Por lo tanto, en la mayoría de los casos, las asignaciones personalizadas se implementan de forma de maximizar el rendimiento y/o minimizar la cantidad de memoria adicional necesaria para asignar objetos pequeños (≤1024 bytes).

2

Para la memoria compartida, es vital que no solo la cabeza del contenedor, sino también los datos que contiene estén almacenados en la memoria compartida.

El asignador de Boost::Interprocess es un buen ejemplo. Sin embargo, como puede leer here, esto no es suficiente, para que todos los contenedores STL sean compatibles con la memoria compartida (debido a las diferentes compensaciones de mapeo en diferentes procesos, los punteros podrían "romperse").

3

Un ejemplo de I vez que he usado estos fue trabajar con sistemas embebidos de recursos muy limitados.Digamos que tienes 2k de RAM libre y tu programa tiene que usar parte de esa memoria. Necesita almacenar las secuencias de decir 4-5 en algún lugar que no está en la pila y, además, necesita tener un acceso muy preciso sobre dónde se almacenan estas cosas, esta es una situación en la que es posible que desee escribir su propio asignador. Las implementaciones predeterminadas pueden fragmentar la memoria, esto podría ser inaceptable si no tiene suficiente memoria y no puede reiniciar su programa.

Un proyecto en el que estaba trabajando utilizaba AVR-GCC en algunos chips de baja potencia. Tuvimos que almacenar 8 secuencias de longitud variable pero con un máximo conocido. El standard library implementation of the memory management es un envoltorio delgado alrededor de malloc/free que realiza un seguimiento de dónde colocar los artículos al anteponer cada bloque de memoria asignado con un puntero justo después del final de esa porción de memoria asignada. Al asignar una nueva pieza de memoria, el asignador estándar debe recorrer cada una de las piezas de la memoria para encontrar el siguiente bloque disponible donde se ajustará el tamaño de memoria requerido. En una plataforma de escritorio esto sería muy rápido para estos pocos elementos, pero hay que tener en cuenta que algunos de estos microcontroladores son muy lentos y primitivos en comparación. Además, el problema de la fragmentación de la memoria era un problema enorme que significaba que realmente no teníamos más remedio que adoptar un enfoque diferente.

Lo que hicimos fue implementar nuestra propia memory pool. Cada bloque de memoria era lo suficientemente grande como para caber en la secuencia más grande que necesitaríamos en él. Esto asignó bloques de memoria de tamaño fijo de antemano y marcó qué bloques de memoria estaban actualmente en uso. Hicimos esto manteniendo un entero de 8 bits donde cada bit representaba si se usaba un cierto bloque. Cambiamos el uso de la memoria aquí por intentar acelerar todo el proceso, lo que en nuestro caso se justificó ya que estábamos empujando este chip del microcontrolador cerca de su máxima capacidad de procesamiento.

Hay una serie de otras veces que puedo ver escribiendo su propio asignador personalizado en el contexto de sistemas integrados, por ejemplo, si la memoria de la secuencia no está en el ram principal como suele ser el caso en these platforms.

1

En una simulación de gráficos, he visto asignadores personalizados utilizados para

  1. Restricciones de alineación que std::allocator no hicieron directamente apoyo.
  2. Minimizando la fragmentación mediante el uso de grupos separados para asignaciones de vida corta (solo este marco) y de larga duración.
1

Hace algún tiempo encontré esta solución muy útil para mí: Fast C++11 allocator for STL containers. Se acelera ligeramente los contenedores STL en VS2017 (~ 5x), así como en GCC (~ 7x). Es un asignador de propósito especial basado en el conjunto de memoria. Se puede usar con contenedores STL solo gracias al mecanismo que está solicitando.

Cuestiones relacionadas