Gestione della memoria per il veloce passaggio di messaggi tra thread in C ++

9

Supponiamo che ci siano due thread, che comunicano tra loro in modo asincrono inviando messaggi di dati. Ogni thread ha qualche tipo di coda di messaggi.

La mia domanda è molto bassa: cosa ci si può aspettare che sia il modo più efficiente per gestire la memoria? Posso pensare a diverse soluzioni:

  1. Mittente crea l'oggetto tramite new . Il destinatario chiama delete .
  2. Raccolta di memoria (per trasferire nuovamente la memoria al mittente)
  3. Raccolta dei rifiuti (ad es., Boehm GC)
  4. (se gli oggetti sono abbastanza piccoli) copia per valore per evitare completamente l'allocazione dell'heap

1) è la soluzione più ovvia, quindi la userò per un prototipo. È probabile che sia già abbastanza buono. Ma indipendentemente dal mio problema specifico, mi chiedo quale tecnica sia più promettente se si sta ottimizzando le prestazioni.

Mi aspetto che il pooling sia teoricamente il migliore, soprattutto perché è possibile utilizzare conoscenze extra sul flusso di informazioni tra i thread. Tuttavia, temo che sia anche il più difficile da ottenere. Un sacco di tuning ...: - (

La raccolta dei rifiuti dovrebbe essere abbastanza semplice da aggiungere in seguito (dopo la soluzione 1), e mi aspetto che funzioni molto bene. Quindi, suppongo che sia la soluzione più pratica se 1) risulta troppo inefficiente.

Se gli oggetti sono piccoli e semplici, la copia per valore potrebbe essere la più veloce. Tuttavia, temo che imponga limitazioni inutili all'implementazione dei messaggi supportati, quindi voglio evitarlo.

    
posta Philipp Claßen 30.12.2012 - 05:17
fonte

4 risposte

9

If the objects are small and simple, copy by value might be the fastest. However, I fear that it forces unnecessary limitations on the implementation of the supported messages, so I want to avoid it.

Se puoi anticipare un limite superiore char buf[256] , ad es. Un'alternativa pratica, se non puoi, che richiama solo le allocazioni dell'heap nei casi rari:

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

    // Points to 'buf' if it fits, heap otherwise.
    char* data;
};
    
risposta data 02.01.2016 - 08:14
fonte
3

Dipenderà da come implementi le code.

Se si utilizza un array (stile round robin) è necessario impostare un limite superiore per la dimensione per la soluzione 4. Se si utilizza una coda collegata, sono necessari oggetti allocati.

Quindi, il pool di risorse può essere fatto facilmente quando si sostituisce solo il nuovo e si elimina con AllocMessage<T> e freeMessage<T> . Il mio suggerimento sarebbe quello di limitare la quantità di potenziali dimensioni che T può avere e arrotondare quando si assegna il messages concreto.

La raccolta dei rifiuti dritta può funzionare ma ciò potrebbe causare lunghe pause quando è necessario raccogliere una parte grande e (penso) eseguire un po 'peggio di nuovo / eliminazione.

    
risposta data 30.12.2012 - 05:39
fonte
3

Se è in C ++, usa solo uno dei puntatori intelligenti - unique_ptr funzionerebbe bene per te , in quanto non eliminerà l'oggetto sottostante finché nessuno ha un handle su di esso. Si passa l'oggetto ptr al ricevitore in base al valore e non è mai necessario preoccuparsi di quale thread deve eliminarlo (nei casi in cui il destinatario non riceve l'oggetto).

Avresti ancora bisogno di gestire il blocco tra i thread, ma le prestazioni saranno buone in quanto non viene copiata alcuna memoria (solo l'oggetto ptr stesso, che è piccolo).

L'allocazione della memoria nell'heap non è la cosa più veloce di sempre, quindi il pooling è utilizzato per rendere questo molto più veloce. Basta prendere il prossimo blocco da un heap pre-dimensionato in un pool, quindi basta usare una libreria esistente per questo.

    
risposta data 30.12.2012 - 13:57
fonte
3

Il più grande successo in termini di prestazioni quando si comunica un oggetto da un thread a un altro è il sovraccarico di un blocco. Questo è nell'ordine di diversi microsecondi, che è significativamente più del tempo medio che impiega una coppia di new / delete (nell'ordine di un centinaio di nanosecondi). Le implementazioni sane new cercano di evitare il blocco a quasi tutti i costi per evitare il loro impatto sulle prestazioni.

Detto questo, devi assicurarti di non aver bisogno di afferrare i lucchetti quando comunichi gli oggetti da un thread a un altro. Conosco due metodi generali per raggiungere questo obiettivo. Entrambi funzionano solo unidirezionalmente tra un mittente e un ricevitore:

  1. Utilizza un buffer circolare. Entrambi i processi controllano un puntatore in questo buffer, uno è il puntatore di lettura, l'altro è il puntatore di scrittura.

    • Il mittente controlla innanzitutto se c'è spazio per aggiungere un elemento confrontando i puntatori, quindi aggiunge l'elemento, quindi incrementa il puntatore di scrittura.

    • Il ricevitore controlla se c'è un elemento da leggere confrontando i puntatori, quindi legge l'elemento, quindi incrementa il puntatore di lettura.

    I puntatori devono essere atomici in quanto sono condivisi tra i thread. Tuttavia, ogni puntatore viene modificato solo da un thread, l'altro richiede solo l'accesso in lettura al puntatore. Gli elementi nel buffer possono essere i puntatori stessi, che ti permettono di ridimensionare facilmente il tuo buffer ad una dimensione che non renderà il blocco del mittente.

  2. Utilizza un elenco collegato che contiene sempre almeno un elemento. Il ricevitore ha un puntatore al primo elemento, il mittente ha un puntatore all'ultimo elemento. Questi puntatori non sono condivisi.

    • Il mittente crea un nuovo nodo per l'elenco collegato, impostando il suo puntatore next su nullptr . Quindi aggiorna il puntatore next dell'ultimo elemento in modo che punti al nuovo elemento. Infine, memorizza il nuovo elemento nel proprio puntatore.

    • Il ricevitore controlla il puntatore next del primo elemento per vedere se sono disponibili nuovi dati. In tal caso, elimina il vecchio primo elemento, avanza il proprio puntatore in modo che punti all'elemento corrente e inizi ad elaborarlo.

    In questa configurazione, i puntatori next devono essere atomici e il mittente deve essere sicuro di non dereferenziare il penultimo elemento dopo aver impostato il suo puntatore next . Il vantaggio è, ovviamente, che il mittente non deve mai bloccare.

Entrambi gli approcci sono molto più veloci di qualsiasi approccio basato su lock, ma richiedono un'attenta implementazione per avere ragione. E, naturalmente, richiedono l'atomicità hardware nativa delle scritture / carichi del puntatore; se la tua implementazione atomic<> utilizza un blocco internamente, sei praticamente condannato.

Allo stesso modo, se hai diversi lettori e / o scrittori, sei praticamente condannato: potresti provare a inventare uno schema senza lock, ma sarà difficile da implementare al meglio. Queste situazioni sono molto più facili da gestire con un lucchetto. Tuttavia, una volta afferrato un lucchetto, puoi smettere di preoccuparti delle prestazioni di new / delete .

    
risposta data 02.01.2016 - 22:42
fonte

Leggi altre domande sui tag