Puede valer la pena almacenar solo un std::string
para todos sus datos combinados y use std::string_view
en el mapa Esto elimina la contención de mutex ya que solo se necesita una asignación de memoria. string_view
tiene un destructor trivial, por lo que no necesita un subproceso para eso.
Utilicé con éxito esta técnica antes para acelerar un programa en un 2500 %, pero eso también se debió a que esta técnica redujo el uso total de la memoria.
Puedes intentar usar un std::vector
para almacenar la memoria. std::vector
los elementos se almacenan de forma contigua, por lo que reducirá la pérdida de memoria caché (ver ¿Qué es un código "amigable con la memoria caché"?)
Entonces tendrás un map<???,size_t>
en lugar de map<???,std::string>
tendrá una indirección más para obtener su cadena (lo que significa un costo de tiempo de ejecución adicional) pero le permitirá iterar en todas las cadenas con menos pérdida de memoria caché.
Sería genial que recrees el problema que estás teniendo con un MVCE y lo muestres:ya sabes, muchas veces el problema que estás pensando es tu problema... no es el problema.
¿Cómo puedo estar seguro de que los 2 problemas de memoria anteriores son la causa (¿alguna herramienta/métrica?)
Dada la información aquí, sugeriría usar un generador de perfiles:gprof (compilar con -g -pg) es el básico. Si tiene el compilador Intel disponible, puede usar vtune.
Hay una versión gratuita de vtune, pero personalmente he usado solo la versión comercial.
Además de esto, puede insertar tiempos en su código:a partir de la descripción textual, no está claro si el tiempo para completar el mapa es comparable al tiempo necesario para borrarlo, o crece constantemente cuando se ejecuta simultáneamente. Yo empezaría con si. Tenga en cuenta que la versión actual de malloc() también está muy optimizada para la concurrencia (¿es esto Linux? Agregue una etiqueta a la pregunta, por favor).
Seguro que cuando borras el mapa hay millones de free()
es llamado por std::~string()
- pero debe asegurarse de que este sea el problema o no:puede usar un mejor enfoque (muchos mencionados en las respuestas/comentarios) o un asignador personalizado respaldado por un enorme bloque de memoria que crea/destruye como una sola unidad.
Si proporciona un MVCE como punto de partida, yo u otros podremos proporcionar una respuesta coherente (todavía no es una respuesta, pero es demasiado larga para ser un comentario)
Solo para aclarar, el programa deliberadamente nunca asigna cosas y libera otras al mismo tiempo, y solo tiene 2 subprocesos, uno dedicado solo para la eliminación.
Tenga en cuenta que cada cadena en el mapa necesita uno (o más) new
y uno delete
(basado en malloc()
y free()
respectivamente), estando las cadenas en las claves o en los valores.
¿Qué tienes en los "valores" del mapa?
Ya que tienes un map<string,<set<int>>
tienes muchas asignaciones:cada vez que realizas un map[string].insert(val)
de una nueva clave, su código llama implícitamente a malloc()
tanto para la cadena como para el conjunto. Incluso si la clave ya está en el mapa, un nuevo int en el conjunto requiere que se asigne un nuevo nodo en el conjunto.
Entonces, tiene muchas asignaciones mientras construye la estructura:su memoria está muy fragmentada en un lado, y su código parece realmente "intensivo en malloc", lo que en principio podría llevar a que las llamadas de memoria mueran de hambre.
Asignaciones/desasignaciones de memoria multiproceso
Una peculiaridad de los subsistemas de memoria modernos es que están optimizados para sistemas multinúcleo:cuando un subproceso asigna memoria en un núcleo, no hay un bloqueo global, sino un bloqueo local de subproceso o local de núcleo para un grupo local de subprocesos. .
Esto significa que cuando un subproceso necesita liberar la memoria asignada por otro, hay un bloqueo no local (más lento).
Esto significa que el mejor enfoque es que cada subproceso asigne/desasigne su propia memoria. Dijo que en principio se puede optimizar mucho su código con estructuras de datos que requieren menos interacciones malloc/free, su código será más local, con respecto a las asignaciones de memoria, si permite que cada subproceso:
- obtener un bloque de datos
- construir el
map<string,<set<int>>
- liberarlo
Y tiene dos hilos que, repetidamente, realizan esta tarea.
NOTA:necesita suficiente RAM para manejar evaluadores concurrentes, pero ahora ya está usando 2 de ellos cargados simultáneamente con un esquema de almacenamiento en búfer doble (uno de llenado, uno de limpieza). ¿Estás seguro de que tu sistema no se está intercambiando debido a un agotamiento de RAM?
Además, este enfoque es escalable:puede usar tantos subprocesos como desee. En su enfoque, estaba limitado a 2 subprocesos:uno construyendo la estructura y otro destruyéndola.
Optimización
Sin un MVCE es una tarea difícil dar instrucciones. Solo ideas que solo sabe si se pueden aplicar en este momento:
- reemplace el conjunto con vector ordenado, reservado en el momento de la creación
- reemplazar las claves del mapa con un vector plano de cadenas ordenadas igualmente espaciadas
- almacene las claves de cadena secuencialmente en un vector plano, agregue hashes para realizar un seguimiento de las claves del mapa. Agregue un mapa hash para realizar un seguimiento del orden de las cadenas en el vector.