Es una especie de eco de la sugerencia de Kylotan, pero recomendaría resolver esto en el nivel de estructura de datos cuando sea posible, no en el nivel inferior del asignador si puede ayudarlo.
Aquí hay un ejemplo simple de cómo puede evitar asignar y liberar Foos
repetidamente utilizando una matriz con agujeros con elementos unidos (resolviendo esto en un nivel de "contenedor" en lugar de un nivel de "asignador"):
struct FooNode
{
explicit FooNode(const Foo& ielement): element(ielement), next(-1) {}
// Stores a 'Foo'.
Foo element;
// Points to the next foo available; either the
// next used foo or the next deleted foo. Can
// use SoA and hoist this out if Foo doesn't
// have 32-bit alignment.
int next;
};
struct Foos
{
// Stores all the Foo nodes.
vector<FooNode> nodes;
// Points to the first used node.
int first_node;
// Points to the first free node.
int free_node;
Foos(): first_node(-1), free_node(-1)
{
}
const FooNode& operator[](int n) const
{
return data[n];
}
void insert(const Foo& element)
{
int index = free_node;
if (index != -1)
{
// If there's a free node available,
// pop it from the free list, overwrite it,
// and push it to the used list.
free_node = data[index].next;
data[index].next = first_node;
data[index].element = element;
first_node = index;
}
else
{
// If there's no free node available, add a
// new node and push it to the used list.
FooNode new_node(element);
new_node.next = first_node;
first_node = data.size() - 1;
data.push_back(new_node);
}
}
void erase(int n)
{
// If the node being removed is the first used
// node, pop it from the used list.
if (first_node == n)
first_node = data[n].next;
// Push the node to the free list.
data[n].next = free_node;
free_node = n;
}
};
Algo en este sentido: una lista de índice individualmente vinculada con una lista libre. Los enlaces de índice le permiten omitir los elementos eliminados, eliminar elementos en tiempo constante y también reclamar / reutilizar / sobrescribir elementos libres con inserción de tiempo constante. Para recorrer la estructura, debes hacer algo como esto:
for (int index = foos.first_node; index != -1; index = foos[index].next)
// do something with foos[index]
Y puede generalizar el tipo anterior de estructura de datos de "matriz vinculada de agujeros" utilizando plantillas, colocación de invocación de dtor nueva y manual para evitar el requisito de asignación de copia, hacer que invoque destructores cuando se eliminan elementos, proporcionar un iterador directo, etc. I elegí mantener el ejemplo muy parecido a C para ilustrar más claramente el concepto y también porque soy muy vago.
Dicho esto, esta estructura tiende a degradarse en la localidad espacial después de eliminar e insertar muchas cosas desde / hacia el medio. En ese punto, los next
enlaces podrían hacer que camine de un lado a otro a lo largo del vector, recargando datos anteriormente desalojados de una línea de caché dentro del mismo recorrido secuencial (esto es inevitable con cualquier estructura de datos o asignador que permita la eliminación en tiempo constante sin barajar elementos mientras reclama espacios desde el medio con inserción de tiempo constante y sin usar algo como un bitset paralelo o unremoved
bandera). Para restaurar la compatibilidad con el caché, puede implementar un copiador y un método de intercambio como este:
Foos(const Foos& other)
{
for (int index = other.first_node; index != -1; index = other[index].next)
insert(foos[index].element);
}
void Foos::swap(Foos& other)
{
nodes.swap(other.nodes):
std::swap(first_node, other.first_node);
std::swap(free_node, other.free_node);
}
// ... then just copy and swap:
Foos(foos).swap(foos);
Ahora la nueva versión es compatible con la caché nuevamente para atravesar. Otro método es almacenar una lista separada de índices en la estructura y ordenarlos periódicamente. Otra es usar un conjunto de bits para indicar qué índices se usan. Eso siempre tendrá que atravesar el conjunto de bits en orden secuencial (para hacer esto de manera eficiente, verifique 64 bits a la vez, por ejemplo, usando FFS / FFZ). El conjunto de bits es el más eficiente y no intrusivo, ya que solo requiere un bit paralelo por elemento para indicar cuáles se utilizan y cuáles se eliminan en lugar de requerir 32 bits.next
índices de , pero es más lento escribir bien (no Sea rápido para el recorrido si está comprobando un bit a la vez: necesita FFS / FFZ para encontrar un bit establecido o no establecido inmediatamente entre más de 32 bits a la vez para determinar rápidamente los rangos de los índices ocupados).
Esta solución vinculada es generalmente la más fácil de implementar y no intrusiva (no requiere modificación Foo
para almacenar algún removed
indicador), lo cual es útil si desea generalizar este contenedor para que funcione con cualquier tipo de datos si no le importa ese 32 bits gastos generales por elemento.
¿Debo crear un grupo de memoria para la asignación dinámica, o no hay necesidad de molestarse con esto? ¿Qué pasa si la plataforma objetivo son dispositivos móviles?
necesitar es una palabra fuerte y estoy predispuesto a trabajar en áreas muy críticas para el rendimiento como el trazado de rayos, el procesamiento de imágenes, las simulaciones de partículas y el procesamiento de malla, pero es relativamente muy costoso asignar y liberar objetos pequeños utilizados para el procesamiento muy ligero como balas y partículas individualmente contra un asignador de memoria de tamaño variable de uso general. Dado que debería poder generalizar la estructura de datos anterior en un día o dos para almacenar lo que quiera, creo que sería un intercambio valioso para eliminar tales costos de asignación / desasignación del montón directamente por pagar por cada cosa pequeña. Además de reducir los costos de asignación / desasignación, obtiene una mejor localidad de referencia que atraviesa los resultados (menos errores de caché y fallas de página, es decir).
En cuanto a lo que Josh mencionó sobre GC, no he estudiado la implementación de GC de C # tan de cerca como la de Java, pero los asignadores de GC a menudo tienen una asignación inicialeso es muy rápido porque está usando un asignador secuencial que no puede liberar memoria del medio (casi como una pila, no puede eliminar cosas del medio). Luego paga los costos costosos para permitir realmente eliminar objetos individuales en un hilo separado copiando la memoria y purgando la memoria anteriormente asignada como un todo (como destruir la pila completa de una vez mientras se copian los datos en algo más como una estructura vinculada), pero debido a que se realiza en un hilo separado, no necesariamente detiene tanto los hilos de su aplicación. Sin embargo, eso conlleva un costo oculto muy significativo de un nivel adicional de indirección y la pérdida general de LOR después de un ciclo inicial de GC. Sin embargo, es otra estrategia para acelerar la asignación: hacerlo más barato en el hilo de llamadas y luego hacer el trabajo costoso en otro. Para eso, necesita dos niveles de indirección para hacer referencia a sus objetos en lugar de uno, ya que terminarán siendo barajados en la memoria entre el tiempo asignado inicialmente y después de un primer ciclo.
Otra estrategia en una línea similar que es un poco más fácil de aplicar en C ++ es simplemente no molestarse en liberar sus objetos en sus hilos principales. Simplemente agregue y agregue y agregue al final de una estructura de datos que no permite eliminar cosas del medio. Sin embargo, marque las cosas que deben eliminarse. Luego, un subproceso separado podría encargarse del costoso trabajo de crear una nueva estructura de datos sin los elementos eliminados y luego intercambiar atómicamente el nuevo con el anterior, por ejemplo, gran parte del costo de asignar y liberar elementos se puede transferir a un hilo separado si puede suponer que solicitar eliminar un elemento no tiene que satisfacerse de inmediato. Eso no solo hace que la liberación sea más barata en lo que respecta a sus hilos, sino que también hace que la asignación sea más barata, ya que puede usar una estructura de datos mucho más simple y más tonta que nunca tiene que manejar casos de eliminación desde el medio. Es como un contenedor que solo necesita unpush_back
función para inserción, una clear
función para eliminar todos los elementos y swap
para intercambiar contenidos con un contenedor nuevo y compacto que excluya los elementos eliminados; eso es todo en cuanto a la mutación.