lenguaje - ¿Ejemplos convincentes de asignadores de C++ personalizados?
lenguaje c++ ejemplos (16)
¿Cuáles son algunas buenas razones para deshacerte de 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?
Los asignadores personalizados siempre han sido una característica de la Biblioteca estándar que no tenía mucha necesidad. Me preguntaba si alguien aquí en SO podría proporcionar algunos ejemplos convincentes para justificar su existencia.
Como menciono here , he visto que el asignador STL personalizado de Intel TBB mejora significativamente el rendimiento de una aplicación multiproceso simplemente cambiando un único
std::vector<T>
a
std::vector<T,tbb::scalable_allocator<T> >
(Esta es una forma rápida y conveniente de cambiar el asignador para usar montones ingeniosos de hilos ingeniosos de TBB; consulte la página 7 en este documento )
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 puede implementarse 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:
- a través de la asignación personalizada, el tiempo de ejecución del acelerador o el controlador recibe una notificación del bloque de memoria
- Además, el sistema operativo puede asegurarse de que el bloque de memoria asignado esté bloqueado por la página (algunos llaman a esta memoria anclada ), es decir, el subsistema de memoria virtual del sistema operativo no puede mover o eliminar la página dentro o desde la memoria
- si 1. y 2. se mantienen y se solicita una transferencia de datos entre un bloque de memoria bloqueado 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 lo hizo mover / eliminarlo
- esto ahorra una copia de memoria que ocurriría con la memoria que se asignó sin bloqueo de página: los datos deben copiarse en la memoria principal a un área de transición de página bloqueada desde donde el acelerador puede inicializar la transferencia de datos (a través de DMA) )
En una simulación de gráficos, he visto asignadores personalizados utilizados para
- Restricciones de alineación que
std::allocator
nostd::allocator
directamente. - Minimizar la fragmentación mediante el uso de grupos separados para asignaciones de corta vida (solo este marco) y de larga duración.
Enlace obligatorio a la charla de CppCon 2015 de Andrei Alexandrescu sobre asignaturas:
https://www.youtube.com/watch?v=LIb3L4vKZ7U
Lo bueno es que solo idearlos te hace pensar en cómo usarlos :-)
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 la memoria como el usuario configurado para usar MySQL, y no "extra".
Estoy trabajando en un asignador mmap que permite que los vectores utilicen 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 (> 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:
#include <memory>
#include <stdio.h>
namespace mmap_allocator_namespace
{
// See replies to this answer for important commentary about inheriting from std::allocator before replicating this code.
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, declare 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 registrar cada vez que se asigna memoria. Lo que es necesario es la estructura de rebind, de lo contrario, el contenedor de vectores utiliza los métodos de asignación / desasignación de las 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.
Estoy usando asignadores personalizados aquí; incluso podría decir que fue para trabajar en 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 hace feliz 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 estamos funcionando en un entorno incrustado: no hay memoria suficiente para realizar la contabilidad de detección de fugas de forma adecuada 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: escriba un asignador personalizado que use el montón ampliado, y úselo solo en el interior de la arquitectura de seguimiento de fugas de memoria ... Todo lo demás pasa por defecto a las sobrecargas normales de nueva / eliminación que realizan el seguimiento de fugas. 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 la misma razón; 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.
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.
Hace algún tiempo encontré esta solución muy útil para mí: asignador de C ++ 11 rápido para contenedores STL . 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.
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 los 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 caso de uso posible 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.
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").
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 sin duda una optimización prematura. Pero puede ser muy útil en ciertos contextos (dispositivos integrados, juegos, etc.).
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 artículo largo y detallado sobre el tema en:
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 usaba 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 de la biblioteca estándar de la gestión de la memoria es una envoltura delgada 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 después del final de esa parte 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.
Entonces, lo que hicimos fue implementar nuestro propio grupo de memoria . 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 varias otras veces en las que puedo ver cómo se escribe su propio asignador personalizado en el contexto de los sistemas integrados, por ejemplo, si la memoria de la secuencia no está en el ram principal, como suele ser el caso en estas plataformas .
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 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 *.
* En realidad, es más complicado que esto, 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.
Yo 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 (de 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 generalmente es solo un contenedor para la asignación C malloc / free, y funciona bien para bloques de memoria más grandes, como más de 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).