Gestión de memoria para el paso rápido de mensajes entre hilos en C ++

9

Supongamos que hay dos hilos que se comunican enviando mensajes de datos de forma asíncrona entre sí. Cada hilo tiene algún tipo de cola de mensajes.

Mi pregunta es de muy bajo nivel: ¿Cuál puede ser la forma más eficiente de administrar la memoria? Se me ocurren varias soluciones:

  1. El remitente crea el objeto a través de new. Receptor de llamadas delete.
  2. Agrupación de memoria (para transferir la memoria al remitente)
  3. Recolección de basura (p. Ej., Boehm GC)
  4. (si los objetos son lo suficientemente pequeños) copie por valor para evitar la asignación del montón por completo

1) es la solución más obvia, así que la usaré para un prototipo. Lo más probable es que ya sea lo suficientemente bueno. Pero independientemente de mi problema específico, me pregunto qué técnica es más prometedora si está optimizando el rendimiento.

Esperaría que la agrupación sea teóricamente la mejor, especialmente porque puede usar un conocimiento adicional sobre el flujo de información entre los hilos. Sin embargo, me temo que también es lo más difícil de hacer bien. Mucha afinación ... :-(

La recolección de basura debería ser bastante fácil de agregar después (después de la solución 1), y esperaría que funcione muy bien. Entonces, supongo que es la solución más práctica si 1) resulta ser demasiado ineficiente.

Si los objetos son pequeños y simples, la copia por valor podría ser la más rápida. Sin embargo, me temo que impone limitaciones innecesarias en la implementación de los mensajes compatibles, por lo que quiero evitarlo.

Philipp Claßen
fuente

Respuestas:

9

Si los objetos son pequeños y simples, la copia por valor podría ser la más rápida. Sin embargo, me temo que impone limitaciones innecesarias en la implementación de los mensajes compatibles, por lo que quiero evitarlo.

Si puede anticipar un límite superior char buf[256], por ejemplo, una alternativa práctica si no puede, que solo invoca asignaciones de montón en los casos excepcionales:

struct Message
{
    // Stores the message data.
    char buf[256];

    // Points to 'buf' if it fits, heap otherwise.
    char* data;
};

fuente
3

Dependerá de cómo implemente las colas.

Si va con una matriz (estilo round robin), debe establecer un límite superior en el tamaño de la solución 4. Si va con una cola vinculada, necesita objetos asignados.

Luego, la agrupación de recursos se puede hacer fácilmente cuando simplemente reemplaza el nuevo y lo elimina con AllocMessage<T>y freeMessage<T>. Mi sugerencia sería limitar la cantidad de tamaños potenciales que Tpuede tener y redondear al asignar concreto messages.

La recolección directa de basura puede funcionar, pero eso puede causar largas pausas cuando necesita recolectar una gran parte, y (creo) funcionará un poco peor que nuevo / eliminar.

monstruo de trinquete
fuente
3

Si está en C ++, solo use uno de los punteros inteligentes: unique_ptr funcionaría bien para usted, ya que no eliminará el objeto subyacente hasta que nadie lo controle. Usted pasa el objeto ptr al receptor por valor y nunca necesita preocuparse sobre qué hilo debería eliminarlo (en los casos en que el receptor no recibe el objeto).

Aún necesitaría manejar el bloqueo entre los subprocesos, pero el rendimiento será bueno ya que no se copiará memoria (solo el objeto ptr en sí, que es pequeño).

Asignar memoria en el montón no es lo más rápido, por lo que la agrupación se usa para hacer esto mucho más rápido. Simplemente tome el siguiente bloque de un montón de tamaño predeterminado en un grupo, así que solo use una biblioteca existente para esto.

gbjbaanb
fuente
2
El bloqueo suele ser un problema mucho mayor que la copia de memoria. Solo digo.
tdammers
Cuando escribes unique_ptr, supongo que quieres decir shared_ptr. Pero aunque no hay duda de que usar un puntero inteligente es bueno para la administración de recursos, no cambia el hecho de que esté utilizando algún tipo de asignación de memoria y desasignación. Creo que esta pregunta es más de bajo nivel.
5gon12eder
3

El mayor impacto en el rendimiento cuando se comunica un objeto de un hilo a otro es la sobrecarga de agarrar un candado. Esto es del orden de varios microsegundos, que es significativamente más que el tiempo promedio que toma un par de new/ delete(del orden de cien nanosegundos). Las newimplementaciones sanas intentan evitar el bloqueo a casi todos los costos para evitar su impacto en el rendimiento.

Dicho esto, desea asegurarse de que no necesita agarrar cerraduras al comunicar los objetos de un hilo a otro. Conozco dos métodos generales para lograr esto. Ambos funcionan solo unidireccionalmente entre un remitente y un receptor:

  1. Use un anillo de amortiguación. Ambos procesos controlan un puntero en este búfer, uno es el puntero de lectura, el otro es el puntero de escritura.

    • El remitente primero verifica si hay espacio para agregar un elemento comparando los punteros, luego agrega el elemento, luego incrementa el puntero de escritura.

    • El receptor comprueba si hay un elemento para leer comparando los punteros, luego lee el elemento y luego incrementa el puntero de lectura.

    Los punteros deben ser atómicos, ya que se comparten entre los hilos. Sin embargo, cada puntero solo es modificado por un hilo, el otro solo necesita acceso de lectura al puntero. Los elementos en el búfer pueden ser punteros, lo que le permite dimensionar fácilmente el búfer de anillo a un tamaño que no bloquee al remitente.

  2. Use una lista vinculada que siempre contenga al menos un elemento. El receptor tiene un puntero al primer elemento, el emisor tiene un puntero al último elemento. Estos punteros no son compartidos.

    • El remitente crea un nuevo nodo para la lista vinculada y establece su nextpuntero en nullptr. Luego actualiza el nextpuntero del último elemento para que apunte al nuevo elemento. Finalmente, almacena el nuevo elemento en su propio puntero.

    • El receptor observa el nextpuntero del primer elemento para ver si hay nuevos datos disponibles. Si es así, elimina el primer elemento anterior, avanza su propio puntero para apuntar al elemento actual y comienza a procesarlo.

    En esta configuración, los nextpunteros deben ser atómicos, y el remitente debe asegurarse de no desreferenciar el segundo último elemento después de haber establecido su nextpuntero. La ventaja es, por supuesto, que el remitente nunca tiene que bloquear.

Ambos enfoques son mucho más rápidos que cualquier enfoque basado en bloqueo, pero requieren una implementación cuidadosa para hacerlo bien. Y, por supuesto, requieren la atomicidad del hardware nativo de las escrituras / cargas del puntero; si su atomic<>implementación usa un bloqueo internamente, está prácticamente condenado.

Del mismo modo, si tiene varios lectores y / o escritores, está prácticamente condenado: puede intentar crear un esquema sin bloqueo, pero será difícil de implementar en el mejor de los casos. Estas situaciones son mucho más fáciles de manejar con un candado. Sin embargo, una vez que agarra una cerradura, puede dejar de preocuparse por new/ deleterendimiento.

cmaster - restablecer monica
fuente
+1 Tengo que revisar esta solución de buffer de anillo como una alternativa a las colas concurrentes usando bucles CAS. Suena muy prometedor.