¿Cuáles son algunas razones realmente buenas para deshacerse std::allocator
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?
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.
Un área donde los asignadores personalizados pueden ser útiles es el desarrollo de juegos, especialmente en consolas de juegos, ya que tienen solo una pequeña cantidad de memoria y no se intercambian. 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 crítico. Otras cosas como los asignadores de grupos pueden ayudar a reducir la fragmentación de la memoria. Puede encontrar un documento largo y detallado sobre el tema en:
EASTL - Biblioteca de plantillas estándar de Electronic Arts
fuente
Estoy trabajando en un mmap-allocator que permite a los vectores usar la memoria de un archivo mapeado en memoria. El objetivo es tener vectores que usen almacenamiento que estén directamente en la memoria virtual mapeados por mmap. Nuestro problema es mejorar la lectura de archivos realmente grandes (> 10 GB) en la memoria sin sobrecarga de copia, por lo tanto, necesito este asignador personalizado.
Hasta ahora tengo el esqueleto de un asignador personalizado (que se deriva de std :: allocator), creo que es un buen punto de partida para escribir asignadores propios. Siéntase libre de usar este código de la forma que desee:
Para usar esto, declare un contenedor STL de la siguiente manera:
Se puede usar, por ejemplo, para iniciar sesión cada vez que se asigna memoria. Lo que es necesario es la estructura de reenlace, de lo contrario, el contenedor de vectores utiliza los métodos de asignación / desasignación de superclases.
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.
fuente
Estoy trabajando con un motor de almacenamiento MySQL 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 memoria como el usuario configuró MySQL para usar, y no "extra".
fuente
Puede ser útil usar asignadores personalizados para usar un grupo de memoria en lugar del montón. Ese es un ejemplo entre muchos otros.
Para la mayoría de los casos, esta es ciertamente una optimización prematura. Pero puede ser muy útil en ciertos contextos (dispositivos integrados, juegos, etc.).
fuente
No he escrito 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 unitaria 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.
fuente
Cuando se trabaja con GPU u otros coprocesadores, a veces es beneficioso asignar estructuras de datos en la memoria principal de una manera especial . Esta forma especial de asignar memoria se puede implementar en un asignador personalizado de manera conveniente.
La razón por la cual la asignación personalizada a través del tiempo de ejecución del acelerador puede ser beneficiosa cuando se usan aceleradores es la siguiente:
fuente
Estoy usando asignadores personalizados aquí; incluso podrías decir que fue para evitar otra administració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 felizmente hace que STL las use para nosotros. Esto nos permite hacer cosas como la agrupación automática de objetos pequeños, detección de fugas, relleno de asignación, relleno libre, asignación de relleno con centinelas, alineación de línea de caché para ciertas asignaciones y liberación retardada.
El problema es que estamos funcionando en un entorno incrustado: no hay suficiente memoria para hacer la contabilidad de 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 otros lugares, a través de funciones de asignación personalizadas.
Solución: escriba un asignador personalizado que use el montón extendido y úselo solo en la parte interna de la arquitectura de seguimiento de fugas de memoria ... Todo lo demás se predetermina a las sobrecargas normales nuevas / eliminadas que hacen el seguimiento de fugas. Esto evita que el rastreador se rastree a sí mismo (y también proporciona un poco de funcionalidad de empaque adicional, sabemos el tamaño de los nodos del rastreador).
También usamos esto para mantener los datos de perfiles de costos de funciones, por la misma razón; escribir una entrada para cada llamada de función y devolución, así como los conmutadores de subprocesos, puede ser costoso rápidamente. El asignador personalizado nuevamente nos da asignaciones más pequeñas en un área de memoria de depuración más grande.
fuente
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 formas de lograrlo, 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.
fuente
Una situación esencial: al escribir código que debe funcionar a través de los límites del módulo (EXE / DLL), es esencial mantener sus asignaciones y eliminaciones en un solo módulo.
Donde me encontré con esto fue 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, cualquier reasignación de la cadena se produzca desde el montón de donde se originó, NO el montón en la DLL que puede ser diferente *.
* En realidad, es más complicado que esto, ya que si se está vinculando dinámicamente al 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.
fuente
Un ejemplo de tiempo cuando los he usado fue trabajar con sistemas embebidos con recursos limitados. Digamos que tienes 2k de RAM gratis y tu programa tiene que usar algo de esa memoria. Debe almacenar, por ejemplo, 4-5 secuencias en algún lugar que no esté en la pila y, además, debe 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 puede ser inaceptable si no tiene suficiente memoria y no puede reiniciar su programa.
Un proyecto en el que estaba trabajando era usar AVR-GCC en algunos chips de baja potencia. Tuvimos que almacenar 8 secuencias de longitud variable pero con un máximo conocido. La implementación estándar de la biblioteca de la gestión de memoria.es un envoltorio delgado alrededor de malloc / free que realiza un seguimiento de dónde colocar los elementos al anteponer cada bloque de memoria asignado con un puntero justo más allá del final de ese pedazo de memoria asignado. Al asignar una nueva pieza de memoria, el asignador estándar tiene que caminar sobre cada una de las piezas de memoria para encontrar el siguiente bloque que esté disponible donde se ajuste el tamaño de memoria solicitado. En una plataforma de escritorio, esto sería muy rápido para estos pocos elementos, pero debe 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.
Entonces, lo que hicimos fue implementar nuestro propio grupo de memoria . Cada bloque de memoria era lo suficientemente grande como para ajustarse a la secuencia más grande que necesitaríamos en él. Esto asignó bloques de memoria de tamaño fijo con anticipación y marcó qué bloques de memoria estaban actualmente en uso. Lo hicimos manteniendo un número entero de 8 bits donde cada bit representaba si se usaba un determinado bloque. Intercambiamos el uso de memoria aquí por intentar acelerar todo el proceso, lo que en nuestro caso se justificó porque estábamos empujando este chip microcontrolador cerca de su capacidad máxima de procesamiento.
Hay varias otras veces que puedo ver escribir su propio asignador personalizado en el contexto de los sistemas integrados, por ejemplo, si la memoria para la secuencia no está en el RAM principal, como podría ser el caso en estas plataformas .
fuente
Enlace obligatorio a la charla CppCon 2015 de Andrei Alexandrescu sobre asignadores:
https://www.youtube.com/watch?v=LIb3L4vKZ7U
Lo bueno es que solo idearlos te hace pensar en ideas sobre cómo los usarías :-)
fuente
Para la memoria compartida es vital que no solo la cabeza del contenedor, sino también los datos que contiene se almacenen en la memoria compartida.
El asignador de Boost :: Interprocess es un buen ejemplo. Sin embargo, como puede leer aquí, todo 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 pueden "romperse").
fuente
Hace algún tiempo encontré esta solución muy útil para mí: asignador rápido C ++ 11 para contenedores STL . Acelera ligeramente los contenedores STL en VS2017 (~ 5x) así como en GCC (~ 7x). Es un asignador de propósito especial basado en un grupo de memoria. Se puede usar con contenedores STL solo gracias al mecanismo que está solicitando.
fuente
Personalmente uso Loki :: Allocator / SmallObject para optimizar el uso de la memoria para objetos pequeños: muestra una buena eficiencia y un rendimiento satisfactorio si tiene que trabajar con cantidades moderadas de objetos realmente pequeños (1 a 256 bytes). Puede ser hasta ~ 30 veces más eficiente que la asignación nueva / eliminación estándar de C ++ si hablamos de asignar cantidades moderadas de objetos pequeños de muchos tamaños diferentes. Además, hay una solución específica de VC llamada "QuickHeap", que brinda el mejor rendimiento posible (las operaciones de asignación y desasignación solo leen y escriben la dirección del bloque que se asigna / devuelve al montón, respectivamente en hasta 99. (9)% de los casos - depende de la configuración y la inicialización), pero a un costo de una sobrecarga notable - necesita dos punteros por extensión y un extra por cada nuevo bloque de memoria. Eso'
El problema con la implementación estándar de C ++ new / delete es que generalmente es solo un contenedor para la asignación 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 adicional utilizada para el mapeo también. Por lo tanto, en la mayoría de los casos, los asignadores personalizados se implementan para maximizar el rendimiento y / o minimizar la cantidad de memoria adicional necesaria para asignar objetos pequeños (≤1024 bytes).
fuente
En una simulación de gráficos, he visto asignadores personalizados utilizados para
std::allocator
no eran directamente compatibles.fuente